diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 54d76b53cb6d..dc4d9a1089fc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/dotnet/.devcontainer/base.Dockerfile -FROM mcr.microsoft.com/dotnet/sdk:8.0 +FROM mcr.microsoft.com/dotnet/sdk:10.0 ARG DOTNET_SDK_INSTALL_URL=https://dot.net/v1/dotnet-install.sh # Install the correct Roslyn SDK into the same directory that the base image installs the SDK in. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 53d71e32eebf..6572e70920e3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,12 +1,22 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: // https://github.com/microsoft/vscode-dev-containers/tree/v0.140.1/containers/dotnetcore { - "name": "C# (.NET 9)", + "name": "Roslyn (.NET 10)", "build": { "dockerfile": "Dockerfile", // Set the context to the workspace folder to allow us to copy files from it. "context": ".." }, + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/powershell:2.0.1": {} + }, + "hostRequirements": { + "cpus": 8, + "memory": "16gb", + "storage": "32gb" + }, + "waitFor": "updateContentCommand", "customizations": { "vscode": { "settings": { @@ -57,5 +67,6 @@ ] } }, - "postCreateCommand": "${containerWorkspaceFolder}/restore.sh" + // Keep restore prebuild-friendly so Codespaces can cache the expensive first-run setup. + "updateContentCommand": "${containerWorkspaceFolder}/.devcontainer/restore-workspace.sh Roslyn.slnx" } diff --git a/.devcontainer/restore-workspace.sh b/.devcontainer/restore-workspace.sh new file mode 100755 index 000000000000..bee203daa318 --- /dev/null +++ b/.devcontainer/restore-workspace.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euo pipefail + +source="${BASH_SOURCE[0]}" + +# resolve $SOURCE until the file is no longer a symlink +while [[ -h $source ]]; do + scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" + source="$(readlink "$source")" + + # if $source was a relative symlink, we need to resolve it relative to the path where the + # symlink file was located + [[ $source != /* ]] && source="$scriptroot/$source" +done + +scriptroot="$( cd -P "$( dirname "$source" )" && pwd )" +reporoot="$( cd -P "$scriptroot/.." && pwd )" + +solution="${1:-Roslyn.slnx}" +if [[ $# -gt 0 ]]; then + shift +fi + +"$reporoot/eng/build.sh" --restore --solution "$solution" "$@" diff --git a/.editorconfig b/.editorconfig index e95967a09e12..15d0eadd18a5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -224,7 +224,7 @@ csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = do_not_ignore +csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 49c955a19b4a..3a1c11ac3e7d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: true contact_links: - name: Report issues related to CAxxxx rules - url: https://github.com/dotnet/roslyn-analyzers/issues/new/choose - about: Enhancements and bug reports to CAxxxx rules are reported to dotnet/roslyn-analyzers repository. + url: https://github.com/dotnet/sdk/issues/new/choose + about: Enhancements and bug reports to CAxxxx rules are reported to dotnet/sdk repository. - name: Suggest language feature url: https://github.com/dotnet/csharplang/issues/new/choose about: Language feature suggestions are discussed in dotnet/csharplang repository first. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 778dc4a44a83..07ee5d58b652 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,30 +3,37 @@ ## Architecture Overview **Core Components** (layered from bottom-up): -- **Compilers** (`src/Compilers/`): C# and VB.NET compilers with syntax trees, semantic models, symbols, and emit APIs +- **Compilers** (`src/Compilers/`): C# and VB.NET compilers — syntax trees, semantic models, symbols, and emit APIs - **Workspaces** (`src/Workspaces/`): Solution/project model, document management, and host services - **Features** (`src/Features/`): Language-agnostic IDE features (refactoring, completion, diagnostics) +- **Analyzers** (`src/Analyzers/`): IDE diagnostic analyzers and code fixes (IDE0xxx) - **EditorFeatures** (`src/EditorFeatures/`): Editor-specific implementations and text buffer integration +- **LanguageServer** (`src/LanguageServer/`): LSP implementation used by VS Code extension - **VisualStudio** (`src/VisualStudio/`): VS-specific language services and UI integration ## Development Workflow **Building**: -- `build.sh` - Full solution build -- `dotnet build Compilers.slnf` - Compiler-only build -- `dotnet msbuild /t:UpdateXlf` - Update .xlf files when their corresponding .resx file is modified +- Windows: `build.cmd` / Unix: `build.sh` — Full solution build +- `dotnet build Compilers.slnf` — Compiler-only build +- `dotnet build Ide.slnf` — IDE-only build +- Solution filters: `Roslyn.slnx` (full), `Compilers.slnf` (compilers), `Ide.slnf` (IDE) **Testing**: -- `test.sh` - Run all tests -- `dotnet test` for specific test projects -- Tests inherit from base classes like `AbstractLanguageServerProtocolTests`, `WorkspaceTestBase` +- Windows: `test.cmd` / Unix: `test.sh` — Run all tests +- `dotnet test ` — Run specific test project +- Tests inherit from base classes: `CSharpTestBase`, `VisualBasicTestBase` (compiler), `AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor` (IDE analyzers) - Use `[UseExportProvider]` for MEF-dependent tests +- Copilot coding agent setup preinstalls `roslyn-language-server` as a global tool and syncs the `dotnet/skills` catalog into `~/.copilot/skills` **Formatting**: - Whitespace formatting preferences are stored in the `.editorconfig` file - When running `dotnet format whitespace` use the `--folder .` option followed by `--include ` to avoid a design-time build -- Apply formatting preferences to any modified .cs or .vb file -- **Important**: Blank lines must not contain any whitespace characters (spaces or tabs). This will cause linting errors that must be fixed. +- **Critical**: Blank lines must not contain any whitespace characters (spaces or tabs). This causes linting errors. + +**Localization**: +- `dotnet msbuild /t:UpdateXlf` — Update `.xlf` files after modifying `.resx` files +- Resource strings accessed via generated designer classes (e.g., `CSharpResources.xxx`, `FeaturesResources.xxx`, `AnalyzersResources.xxx`) ## Code Patterns @@ -40,55 +47,62 @@ internal sealed class CSharpMyService : IMyService **Roslyn API Usage**: ```csharp -// Always use immutable patterns -var newTree = oldTree.WithChangedText(newText); +// All syntax trees, documents, and solutions are immutable — use With* methods var newDocument = oldDocument.WithSyntaxTree(newTree); -// Semantic analysis +// Semantic analysis — always pass CancellationToken var semanticModel = await document.GetSemanticModelAsync(cancellationToken); var symbolInfo = semanticModel.GetSymbolInfo(expression); ``` **Testing Conventions**: -- Inherit from `TestBase` or language-specific base classes -- Use `UseExportProvider` for MEF services -- Test utilities in `Microsoft.CodeAnalysis.Test.Utilities` -- Language-specific test bases: `CSharpTestBase`, `VisualBasicTestBase` -- Add `[WorkItem("https://github.com/dotnet/roslyn/issues/issueNumber")]` attribute to tests that fix specific GitHub issues +- Add `[WorkItem("https://github.com/dotnet/roslyn/issues/NNN")]` attribute to tests that fix specific GitHub issues - Prefer raw string literals (`"""..."""`) over verbatim strings (`@"..."`) when creating test source code -- Avoid unnecessary intermediary assertions - tests should do the minimal amount of work to validate just the core issue being addressed - - In tests, use concise methods like `.Single()` instead of asserting count and extracting elements - - For compiler tests, validate diagnostics (e.g., `comp.VerifyEmitDiagnostics()`) so reviewers can easily see if the code is in error or represents something legal - -## Critical Integration Points - -- **Language Server Protocol**: `src/LanguageServer/` contains LSP implementation used by VS Code extension -- **ServiceHub**: Remote services (`src/Workspaces/Remote/`) run out-of-process for performance -- **Analyzers**: `src/Analyzers/` for static analysis, separate from `src/RoslynAnalyzers/` (internal tooling) -- **VSIX Packaging**: Multiple deployment targets - `src/VisualStudio/Setup/` for main VS integration +- Keep tests focused — do minimal work to validate the core issue + - Use `.Single()` instead of asserting count and extracting elements + - For compiler tests, use `comp.VerifyEmitDiagnostics()` so reviewers can see if code is legal + - For IDE tests, use `TestInRegularAndScriptAsync` / `TestMissingInRegularAndScriptAsync` ## Key Conventions - **Namespace Strategy**: `Microsoft.CodeAnalysis.[Language].[Area]` (e.g., `Microsoft.CodeAnalysis.CSharp.Formatting`) -- **File Organization**: Group by feature area, separate language-specific implementations -- **Immutability**: All syntax trees, documents, and solutions are immutable - create new instances for changes +- **Immutability**: All syntax trees, documents, and solutions are immutable — create new instances for changes - **Cancellation**: Always thread `CancellationToken` through async operations -- **MEF Lifecycle**: Use `[ImportingConstructor]` with obsolete attribute for MEF v2 compatibility -- **PROTOTYPE Comments**: Only used to track follow-up work in feature branches and are disallowed in main branch -- **Code Formatting**: Avoid trailing spaces and blank lines (lines with only whitespace). Ensure all lines either have content or are completely empty. +- **MEF Lifecycle**: Use `[ImportingConstructor]` with `[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]` +- **Null checks**: Use `Contract.ThrowIfNull()` instead of manual null checks +- **Private fields**: `_camelCase` naming +- **PROTOTYPE Comments**: Only used to track follow-up work in feature branches — disallowed in main branch +- **Code Formatting**: Avoid trailing spaces. Blank lines must be completely empty (no whitespace characters). +- **Public API Tracking**: Update `PublicAPI.Unshipped.txt` when adding/changing public APIs + +## Code Generation + +Several core data structures are generated from XML definitions — never edit generated `.cs` files directly: +- **Syntax trees**: `src/Compilers/CSharp/Portable/Syntax/Syntax.xml` +- **Bound trees**: `src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml` +- **After modifying XML files**, run: `dotnet run --file eng/generate-compiler-code.cs` ## Common Gotchas -- Follow existing conventions in the file +- Follow existing conventions in the file you're editing - Language services must be exported per-language, not shared across C#/VB -- Test failures often indicate MEF composition issues - check export attributes -- VSIX deployment targets multiple architectures - ensure platform-specific assets are handled -- ServiceHub components require special deployment considerations for .NET Core vs Framework +- Test failures often indicate MEF composition issues — check export attributes +- ServiceHub components (`src/Workspaces/Remote/`) require special deployment considerations for .NET Core vs Framework +- IDE analyzers should inherit from `AbstractBuiltInCodeStyleDiagnosticAnalyzer` for code style diagnostics, not raw `DiagnosticAnalyzer` +- Always provide `FixAllProvider` (typically `WellKnownFixAllProviders.BatchFixer`) for code fixes + +## Documentation + +**Creating new docs**: +- Use **kebab-case** for file names (e.g., `roslyn-language-server-copilot-plugin.md`, not `Roslyn Language Server Copilot Plugin.md`) +- Place docs in the appropriate subdirectory under `docs/` (e.g., `docs/contributing/`, `docs/compilers/`, `docs/features/`) +- General docs that don't fit a subdirectory go directly in `docs/` ## Essential Files for Context -- `docs/wiki/Roslyn-Overview.md` - Architecture deep-dive -- `docs/contributing/Building, Debugging, and Testing on Unix.md` - Development setup -- `src/Compilers/Core/Portable/` - Core compiler APIs -- `src/Workspaces/Core/Portable/` - Workspace object model -- Solution filters: `Roslyn.slnx`, `Compilers.slnf`, `Ide.slnf` for focused builds +- `src/Compilers/CSharp/Portable/Errors/ErrorCode.cs` — All C# compiler error codes +- `src/Compilers/CSharp/Portable/Errors/MessageID.cs` — Language feature version gating +- `src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs` — All IDE diagnostic ID constants +- `src/Compilers/CSharp/Portable/Syntax/Syntax.xml` — Syntax tree node definitions +- `src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml` — Bound tree node definitions +- `docs/wiki/Roslyn-Overview.md` — Architecture deep-dive diff --git a/.github/instructions/Compiler.instructions.md b/.github/instructions/Compiler.instructions.md index 770a07247dfb..40a4e0cb74a8 100644 --- a/.github/instructions/Compiler.instructions.md +++ b/.github/instructions/Compiler.instructions.md @@ -44,6 +44,7 @@ public class MyTests : CSharpTestBase ## Build & Test Workflows ### Essential Build Commands + ```powershell # Full build (use VS Code tasks when available) ./build.sh diff --git a/.github/instructions/IDE.instructions.md b/.github/instructions/IDE.instructions.md index 87bde507b025..337c37197245 100644 --- a/.github/instructions/IDE.instructions.md +++ b/.github/instructions/IDE.instructions.md @@ -4,151 +4,112 @@ applyTo: "src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures,VisualStud # Roslyn IDE Development Guide -This guide provides essential knowledge for working effectively with Roslyn's IDE-focused codebase. - ## Architecture Overview Roslyn uses a **layered service architecture** built on MEF (Managed Extensibility Framework): -- **Workspaces** (`src/Workspaces/`): Core abstractions - `Workspace`, `Solution`, `Project`, `Document` +- **Workspaces** (`src/Workspaces/`): Core abstractions — `Workspace`, `Solution`, `Project`, `Document` - **Features** (`src/Features/`): Language-agnostic IDE features (refactoring, navigation, completion) +- **Analyzers** (`src/Analyzers/`): IDE diagnostic analyzers and code fixes (IDE0xxx diagnostics) - **LanguageServer** (`src/LanguageServer/`): Shared LSP protocol implementation and Roslyn LSP executable - **EditorFeatures** (`src/EditorFeatures/`): VS Editor integration and text manipulation - **VisualStudio** (`src/VisualStudio/`): Visual Studio-specific implementations -### Service Resolution Pattern - +### Service Resolution ```csharp -// Get workspace services +// Workspace services var service = workspace.Services.GetRequiredService(); -// Get language-specific services +// Language-specific services var csharpService = workspace.Services.GetLanguageServices(LanguageNames.CSharp) .GetRequiredService(); - -// In tests, use ExportProvider directly -var service = ExportProvider.GetExportedValue(); ``` ### MEF Export Patterns - ```csharp -// Workspace service +// Workspace service (language-agnostic) [ExportWorkspaceService(typeof(IMyService)), Shared] internal class MyService : IMyService { } -// Language service +// Language service (per-language — never share across C#/VB) [ExportLanguageService(typeof(IMyService), LanguageNames.CSharp), Shared] internal class CSharpMyService : IMyService { } -// Always use ImportingConstructor with obsolete warning +// Constructor — always include both attributes [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public MyService(IDependency dependency) { } ``` -## Key Development Patterns +## Resource & Localization -### TestAccessor Pattern -For exposing internal state to tests without making it public: +- UI strings live in `.resx` files (e.g., `AnalyzersResources.resx`, `FeaturesResources.resx`, `WorkspacesResources.resx`) +- Reference via generated designer class: `FeaturesResources.Some_string` +- For localizable strings: `new LocalizableResourceString(nameof(FeaturesResources.Some_string), FeaturesResources.ResourceManager, typeof(FeaturesResources))` +- After modifying `.resx` files, run `dotnet msbuild /t:UpdateXlf` to update `.xlf` localization files + +## Testing Patterns +### Test Workspace (MEF-dependent tests) ```csharp -internal class ProductionClass +[UseExportProvider] +public class MyTests { - private int _privateField; - - internal TestAccessor GetTestAccessor() => new(this); - - internal readonly struct TestAccessor + [Fact] + public async Task TestSomething() { - private readonly ProductionClass _instance; - internal TestAccessor(ProductionClass instance) => _instance = instance; - internal ref int PrivateField => ref _instance._privateField; + var workspace = EditorTestWorkspace.CreateCSharp("class C { }"); + var document = workspace.Documents.Single(); } } ``` -### Diagnostic Analyzer Structure +### Test Conventions +- Prefer raw string literals (`"""..."""`) over verbatim strings (`@"..."`) for test source code +- Add `[WorkItem("https://github.com/dotnet/roslyn/issues/NNN")]` for tests fixing specific issues +- Keep tests focused — avoid unnecessary intermediary assertions +- Use `[UseExportProvider]` for any test that depends on MEF services + +## Key Development Patterns + +### TestAccessor Pattern +Expose internal state to tests without making it public: ```csharp -[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] -public sealed class MyAnalyzer : DiagnosticAnalyzer +internal class ProductionClass { - private static readonly DiagnosticDescriptor s_rule = new( - "MyAnalyzer001", "Title", "Message format", "Category", - DiagnosticSeverity.Warning, isEnabledByDefault: true); + private int _privateField; - public override ImmutableArray SupportedDiagnostics - => ImmutableArray.Create(s_rule); + internal TestAccessor GetTestAccessor() => new(this); - public override void Initialize(AnalysisContext context) + internal readonly struct TestAccessor { - context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); + private readonly ProductionClass _instance; + internal TestAccessor(ProductionClass instance) => _instance = instance; + internal ref int PrivateField => ref _instance._privateField; } } ``` +**TestAccessor calls are forbidden in production code** — enforced by analyzer RS0043. -## Essential Build & Test Commands - -```bash -# Full build -.build.sh - -# Run specific test project -dotnet test src/EditorFeatures/Test/ - -# Build with analyzers -.build.sh -testUsedAssemblies - -# Generate compiler code (if changing syntax) -dotnet run --file eng/generate-compiler-code.cs -``` - -## Working with Tests - -### Test Workspace Creation +### SyntaxGenerator (Language-Agnostic Code Generation) +Use `SyntaxGenerator` to generate code without language-specific knowledge: ```csharp -[UseExportProvider] -public class MyTests -{ - [Fact] - public async Task TestSomething() - { - var workspace = EditorTestWorkspace.CreateCSharp("class C { }"); - var document = workspace.Documents.Single(); - // Test logic here - } -} +var generator = SyntaxGenerator.GetGenerator(document); +var methodDecl = generator.MethodDeclaration("MyMethod", ...); ``` -### Common Test Utilities -- `DescriptorFactory.CreateSimpleDescriptor()` - Create test diagnostic descriptors -- `VerifyCS.VerifyAnalyzerAsync()` - Verify C# analyzer behavior -- `TestWorkspace.CreateCSharp()` - Create test workspaces -- `UseExportProviderAttribute` - Required for MEF-dependent tests - ## Coding Conventions -### Performance Rules -- **Avoid LINQ in hot paths** - Use manual loops in compiler/analyzer code -- **Avoid `foreach` over non-struct enumerators** - Use `for` loops or `.AsSpan()` -- **Use object pooling** - See `ObjectPool` usage patterns -- **Prefer `ReadOnlySpan`** over `IEnumerable` for performance-critical APIs - -### Naming Conventions -- Private fields: `_camelCase` -- Internal test accessors: `GetTestAccessor()` returning `TestAccessor` struct -- Diagnostic IDs: Consistent prefixes (RS, CA, IDE followed by numbers) -- MEF exports: Match interface names without "I" prefix - -### Resource Management -- MEF services are automatically disposed by the container -- Use `TestAccessor` pattern instead of `internal` accessibility for test-only APIs -- Always implement `IDisposable` for stateful services +- **Private fields**: `_camelCase` +- **Naming**: MEF exports match interface names without "I" prefix +- **Null checks**: Use `Contract.ThrowIfNull()` instead of manual null checks +- **Immutability**: All `Document`, `Solution`, `Project` instances are immutable — use `With*` methods +- **Cancellation**: Always thread `CancellationToken` through async operations +- **Performance**: Avoid LINQ in hot paths, prefer `for` loops or `.AsSpan()`, use `ObjectPool` ## Common Gotchas - **ImportingConstructor must be marked `[Obsolete]`** with `MefConstruction.ImportingConstructorMessage` -- **Use `Contract.ThrowIfNull()`** instead of manual null checks in public APIs -- **TestAccessor calls are forbidden in production code** - enforced by analyzer RS0043 -- **Language services must be exported with specific language name** - don't use generic exports -- **Workspace changes must use immutable updates** - call `Workspace.SetCurrentSolution()` appropriately \ No newline at end of file +- **Language services must be exported with a specific language name** — don't use generic exports for both C#/VB +- **Workspace changes must use immutable updates** — `Workspace.SetCurrentSolution()` +- **Test failures often indicate MEF composition issues** — check export attributes \ No newline at end of file diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml index 3fa646c30860..5c72b4036db3 100644 --- a/.github/policies/resourceManagement.yml +++ b/.github/policies/resourceManagement.yml @@ -311,6 +311,15 @@ configuration: You can refer to the [.NET SDK breaking change guidelines](https://github.com/dotnet/sdk/blob/main/documentation/project-docs/breaking-change-guidelines.md) - + + - description: Add a CodeFlow link to new pull requests + if: + - payloadType: Pull_Request + - isAction: + action: Opened + then: + - addCodeFlowLink + onFailure: onSuccess: + diff --git a/.github/skills/analyzer-codefix/SKILL.md b/.github/skills/analyzer-codefix/SKILL.md new file mode 100644 index 000000000000..8aabda751303 --- /dev/null +++ b/.github/skills/analyzer-codefix/SKILL.md @@ -0,0 +1,208 @@ +--- +name: analyzer-codefix +description: "Create or modify Roslyn IDE analyzers, code fixes, and code refactorings. Use when: adding a new IDE diagnostic (IDE0xxx), implementing a CodeFixProvider, implementing a CodeRefactoringProvider, writing analyzer/fixer tests, or working with AbstractBuiltInCodeStyleDiagnosticAnalyzer. Also use for: diagnostic analyzer, code action, FixAllProvider, TestInRegularAndScriptAsync, TestMissingInRegularAndScriptAsync." +--- + +# Roslyn Analyzer, Code Fix & Code Refactoring Patterns + +## When to Use + +- Adding a new IDE analyzer (IDE0xxx diagnostic) +- Implementing or modifying a `CodeFixProvider` +- Implementing or modifying a `CodeRefactoringProvider` +- Writing tests for analyzers, code fixes, or refactorings +- Working with `AbstractBuiltInCodeStyleDiagnosticAnalyzer` + +## IDE Diagnostic IDs + +All IDE diagnostics use `IDE0xxx` format, defined as constants in `src/Analyzers/Core/Analyzers/IDEDiagnosticIds.cs`. Always reference these constants rather than hardcoding string IDs. + +## Analyzer Patterns + +### Code Style Analyzer (preferred for IDE diagnostics) + +Inherit from `AbstractBuiltInCodeStyleDiagnosticAnalyzer` — not raw `DiagnosticAnalyzer`: + +```csharp +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CSharpUseXxxDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer +{ + public CSharpUseXxxDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseXxxDiagnosticId, + EnforceOnBuildValues.UseXxx, + option: CSharpCodeStyleOptions.PreferXxx, + title: new LocalizableResourceString( + nameof(AnalyzersResources.Use_xxx), + AnalyzersResources.ResourceManager, + typeof(AnalyzersResources))) { } + + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterCompilationStartAction(context => + context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation)); +} +``` + +### Non-Style Analyzer + +Use raw `DiagnosticAnalyzer` with a `DiagnosticDescriptor`: + +```csharp +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public sealed class MyAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor s_rule = new( + "IDE0xxx", "Title", "Message format", "Category", + DiagnosticSeverity.Warning, isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(s_rule); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.ClassDeclaration); + } +} +``` + +## CodeFixProvider Pattern + +```csharp +[ExportCodeFixProvider(LanguageNames.CSharp, Name = PredefinedCodeFixProviderNames.UseXxx), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpUseXxxCodeFixProvider() : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds + => [IDEDiagnosticIds.UseXxxDiagnosticId]; + + // Always provide FixAllProvider — typically BatchFixer + public override FixAllProvider? GetFixAllProvider() + => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.First(); + context.RegisterCodeFix( + CodeAction.Create( + title, + c => FixAsync(context.Document, diagnostic, c), + equivalenceKey: nameof(AnalyzersResources.Use_xxx)), + diagnostic); + } +} +``` + +## CodeRefactoringProvider Pattern + +```csharp +[ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.MyRefactoring), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class MyRefactoringProvider() : CodeRefactoringProvider +{ + public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) + { + // Register refactoring actions via context.RegisterRefactoring(...) + } +} +``` + +## EditorConfig / Options Integration + +IDE analyzers read user preferences from `.editorconfig` via the options system: +- Options are defined in `CSharpCodeStyleOptions`, `CodeStyleOptions2`, etc. +- Access in analyzers: `context.GetCSharpAnalyzerOptions().PreferXxx` +- Analyzers should respect user-configured severity and enablement + +## Resource & Localization + +- Error messages and UI strings live in `.resx` files (e.g., `AnalyzersResources.resx`, `FeaturesResources.resx`) +- Reference via generated designer class: `AnalyzersResources.Use_xxx` +- For localizable strings in descriptors: `new LocalizableResourceString(nameof(AnalyzersResources.Use_xxx), AnalyzersResources.ResourceManager, typeof(AnalyzersResources))` +- After modifying `.resx` files, run `dotnet msbuild /t:UpdateXlf` to update `.xlf` localization files + +## Testing + +### Test Base Class + +For analyzer + fixer pairs, inherit from `AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor`: + +```csharp +public sealed class UseXxxTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor +{ + internal override (DiagnosticAnalyzer, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace) + => (new CSharpUseXxxDiagnosticAnalyzer(), new CSharpUseXxxCodeFixProvider()); + + [Fact] + public async Task TestBasicCase() + { + await TestInRegularAndScriptAsync( + """ + class C + { + void M() + { + [|var x = 1;|] + } + } + """, + """ + class C + { + void M() + { + int x = 1; + } + } + """); + } + + [Fact] + public async Task TestNoFixWhenAlreadyCorrect() + { + await TestMissingInRegularAndScriptAsync( + """ + class C + { + void M() + { + int x = 1; + } + } + """); + } +} +``` + +### Test Markup Syntax +- `[|...|]` — diagnostic span (the code the diagnostic highlights) +- `{|DiagnosticId:...|}` — named span with specific diagnostic ID +- `$$` — cursor position marker (single instance only) + +### Verifier-Based Tests (for standalone analyzers) + +```csharp +await new VerifyCS.Test +{ + TestCode = source, + FixedCode = fixedSource, + ExpectedDiagnostics = { VerifyCS.Diagnostic("IDE0xxx").WithSpan(5, 9, 5, 20) }, +}.RunAsync(); +``` + +### Test Conventions +- Use `TestInRegularAndScriptAsync` to cover both regular and script contexts +- Use `TestMissingInRegularAndScriptAsync` to verify no fix is offered +- Prefer raw string literals (`"""..."""`) over verbatim strings (`@"..."`) for test source code +- Add `[WorkItem("https://github.com/dotnet/roslyn/issues/NNN")]` for tests fixing specific issues +- Keep tests focused — avoid unnecessary intermediary assertions + +## Checklist for New Analyzer + Code Fix + +1. Add diagnostic ID constant to `IDEDiagnosticIds.cs` +2. Add resource strings to the appropriate `.resx` file +3. Run `dotnet msbuild /t:UpdateXlf` for localization +4. Implement analyzer (inherit `AbstractBuiltInCodeStyleDiagnosticAnalyzer` for code style) +5. Implement code fix with `FixAllProvider` +6. Write tests inheriting `AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor` +7. Test both "fix applies" and "fix does not apply" cases diff --git a/.github/skills/ci-analysis/SKILL.md b/.github/skills/ci-analysis/SKILL.md new file mode 100644 index 000000000000..5b42f8363ee1 --- /dev/null +++ b/.github/skills/ci-analysis/SKILL.md @@ -0,0 +1,259 @@ +--- +name: ci-analysis +description: Analyze CI build and test status from Azure DevOps and Helix for dotnet repository PRs. Use when checking CI status, investigating failures, determining if a PR is ready to merge, or given URLs containing dev.azure.com or helix.dot.net. Also use when asked "why is CI red", "test failures", "retry CI", "rerun tests", "is CI green", "build failed", "checks failing", or "flaky tests". +--- + +# Azure DevOps and Helix CI Analysis + +Analyze CI build status and test failures in Azure DevOps and Helix for dotnet repositories (runtime, sdk, aspnetcore, roslyn, and more). + +> 🚨 **NEVER** use `gh pr review --approve` or `--request-changes`. Only `--comment` is allowed. Approval and blocking are human-only actions. + +**Workflow**: Gather PR context (Step 0) → run the script → read the human-readable output + `[CI_ANALYSIS_SUMMARY]` JSON → synthesize recommendations yourself. The script collects data; you generate the advice. For supplementary investigation beyond the script, MCP tools (AzDO, Helix, GitHub) provide structured access when available; the script and `gh` CLI work independently when they're not. + +## When to Use This Skill + +Use this skill when: +- Checking CI status on a PR ("is CI passing?", "what's the build status?", "why is CI red?") +- Investigating CI failures or checking why a PR's tests are failing +- Determining if a PR is ready to merge based on CI results +- Debugging Helix test issues or analyzing build errors +- Given URLs containing `dev.azure.com`, `helix.dot.net`, or GitHub PR links with failing checks +- Asked questions like "why is this PR failing", "analyze the CI", "is CI green", "retry CI", "rerun tests", or "test failures" +- Investigating canceled or timed-out jobs for recoverable results + +## Script Limitations + +The `Get-CIStatus.ps1` script targets **Azure DevOps + Helix** infrastructure specifically. It won't help with: +- **GitHub Actions** workflows (different API, different log format) +- Repos not using **Helix** for test distribution (no Helix work items to query) +- Pure **build performance** questions (use MSBuild binlog analysis instead) + +However, the analysis patterns in this skill (interpreting failures, correlating with PR changes, distinguishing infrastructure vs. code issues) apply broadly even outside AzDO/Helix. + +## Quick Start + +```powershell +# Analyze PR failures (most common) - defaults to dotnet/runtime +./scripts/Get-CIStatus.ps1 -PRNumber 123445 -ShowLogs + +# Analyze by build ID +./scripts/Get-CIStatus.ps1 -BuildId 1276327 -ShowLogs + +# Query specific Helix work item +./scripts/Get-CIStatus.ps1 -HelixJob "4b24b2c2-..." -WorkItem "System.Net.Http.Tests" + +# Other dotnet repositories +./scripts/Get-CIStatus.ps1 -PRNumber 12345 -Repository "dotnet/aspnetcore" +./scripts/Get-CIStatus.ps1 -PRNumber 67890 -Repository "dotnet/sdk" +./scripts/Get-CIStatus.ps1 -PRNumber 11111 -Repository "dotnet/roslyn" +``` + +## Key Parameters + +| Parameter | Description | +|-----------|-------------| +| `-PRNumber` | GitHub PR number to analyze | +| `-BuildId` | Azure DevOps build ID | +| `-ShowLogs` | Fetch and display Helix console logs | +| `-Repository` | Target repo (default: dotnet/runtime) | +| `-MaxJobs` | Max failed jobs to show (default: 5) | +| `-SearchMihuBot` | Search MihuBot for related issues | + +## Three Modes + +The script operates in three distinct modes depending on what information you have: + +| You have... | Use | What you get | +|-------------|-----|-------------| +| A GitHub PR number | `-PRNumber 12345` | Full analysis: all builds, failures, known issues, structured JSON summary | +| An AzDO build ID | `-BuildId 1276327` | Single build analysis: timeline, failures, Helix results | +| A Helix job ID (optionally a specific work item) | `-HelixJob "..." [-WorkItem "..."]` | Deep dive: list work items for the job, or with `-WorkItem`, focus on a single work item's console logs, artifacts, and test results | + +> ❌ **Don't guess the mode.** If the user gives a PR URL, use `-PRNumber`. If they paste an AzDO build link, extract the build ID. If they reference a specific Helix job, use `-HelixJob`. + +## What the Script Does + +### PR Analysis Mode (`-PRNumber`) +1. Discovers AzDO builds associated with the PR (from GitHub check status; for full build history, query AzDO builds on `refs/pull/{PR}/merge` branch) +2. Fetches Build Analysis for known issues +3. Gets failed jobs from Azure DevOps timeline +4. **Separates canceled jobs from failed jobs** (canceled may be dependency-canceled or timeout-canceled) +5. Extracts Helix work item failures from each failed job +6. Fetches console logs (with `-ShowLogs`) +7. Searches for known issues with "Known Build Error" label +8. Correlates failures with PR file changes +9. **Emits structured summary** — `[CI_ANALYSIS_SUMMARY]` JSON block with all key facts for the agent to reason over + +> **After the script runs**, you (the agent) generate recommendations. The script collects data; you synthesize the advice. See [Generating Recommendations](#generating-recommendations) below. + +### Build ID Mode (`-BuildId`) +1. Fetches the build timeline directly (skips PR discovery) +2. Performs steps 3–7 from PR Analysis Mode, but does **not** fetch Build Analysis known issues or correlate failures with PR file changes (those require a PR number). Still emits `[CI_ANALYSIS_SUMMARY]` JSON. + +### Helix Job Mode (`-HelixJob` [and optional `-WorkItem`]) +1. With `-HelixJob` alone: enumerates work items for the job and summarizes their status +2. With `-HelixJob` and `-WorkItem`: queries the specific work item for status and artifacts +3. Fetches console logs and file listings, displays detailed failure information + +## Interpreting Results + +**Known Issues section**: Failures matching existing GitHub issues - these are tracked and being investigated. + +**Build Analysis check status**: The "Build Analysis" GitHub check is **green** only when *every* failure is matched to a known issue. If it's **red**, at least one failure is unaccounted for — do NOT claim "all failures are known issues" just because some known issues were found. You must verify each failing job is covered by a specific known issue before calling it safe to retry. + +**Canceled/timed-out jobs**: Jobs canceled due to earlier stage failures or AzDO timeouts. Dependency-canceled jobs don't need investigation. **Timeout-canceled jobs may have all-passing Helix results** — the "failure" is just the AzDO job wrapper timing out, not actual test failures. To verify: use `hlx_status` on each Helix job in the timed-out build (include passed work items). If all work items passed, the build effectively passed. + +> ❌ **Don't dismiss timed-out builds.** A build marked "failed" due to a 3-hour AzDO timeout can have 100% passing Helix work items. Check before concluding it failed. + +**PR Change Correlation**: Files changed by PR appearing in failures - likely PR-related. + +**Build errors**: Compilation failures need code fixes. + +**Helix failures**: Test failures on distributed infrastructure. + +**Local test failures**: Some repos (e.g., dotnet/sdk) run tests directly on build agents. These can also match known issues - search for the test name with the "Known Build Error" label. + +**Per-failure details** (`failedJobDetails` in JSON): Each failed job includes `errorCategory`, `errorSnippet`, and `helixWorkItems`. Use these for per-job classification instead of applying a single `recommendationHint` to all failures. + +Error categories: `test-failure`, `build-error`, `test-timeout`, `crash` (exit codes 139/134/-4), `tests-passed-reporter-failed` (all tests passed but reporter crashed — genuinely infrastructure), `unclassified` (investigate manually). + +> ⚠️ **`crash` does NOT always mean tests failed.** Exit code -4 often means the Helix work item wrapper timed out *after* tests completed. Always check `testResults.xml` before concluding a crash is a real failure. See [Recovering Results from Crashed/Canceled Jobs](#recovering-results-from-crashedcanceled-jobs). + +> ⚠️ **Be cautious labeling failures as "infrastructure."** Only conclude infrastructure with strong evidence: Build Analysis match, identical failure on target branch, or confirmed outage. Exception: `tests-passed-reporter-failed` is genuinely infrastructure. + +> ❌ **Missing packages on flow PRs ≠ infrastructure.** Flow PRs can cause builds to request *different* packages. Check *which* package and *why* before assuming feed delay. + +### Recovering Results from Crashed/Canceled Jobs + +When an AzDO job is canceled (timeout) or Helix work items show `Crash` (exit code -4), the tests may have actually passed. Follow this procedure: + +1. **Find the Helix job IDs** — Read the AzDO "Send to Helix" step log and search for lines containing `Sent Helix Job`. Extract the job GUIDs. + +2. **Check Helix job status** — Get pass/fail summary for each job. Look at `failedCount` vs `passedCount`. + +3. **For work items marked Crash/Failed** — Check if tests actually passed despite the crash. Try structured test results first (TRX parsing), then search for pass/fail counts in result files without downloading, then download as last resort: + - Parse the XML: `total`, `passed`, `failed` attributes on the `` element + - If `failed=0` and `passed > 0`, the tests passed — the "crash" is the wrapper timing out after test completion + +4. **Verdict**: + - All work items passed or crash-with-passing-results → **Tests effectively passed.** The failure is infrastructure (wrapper timeout). + - Some work items have `failed > 0` in testResults.xml → **Real test failures.** Investigate those specific tests. + - No testResults.xml uploaded → Tests may not have run at all. Check console logs for errors. + +> This pattern is common with long-running test suites (e.g., WasmBuildTests) where tests complete but the Helix work item wrapper exceeds its timeout during result upload or cleanup. + +## Generating Recommendations + +After the script outputs the `[CI_ANALYSIS_SUMMARY]` JSON block, **you** synthesize recommendations. Do not parrot the JSON — reason over it. + +### Decision logic + +Read `recommendationHint` as a starting point, then layer in context: + +| Hint | Action | +|------|--------| +| `BUILD_SUCCESSFUL` | No failures. Confirm CI is green. | +| `KNOWN_ISSUES_DETECTED` | Known tracked issues found — but this does NOT mean all failures are covered. Check the Build Analysis check status: if it's red, some failures are unmatched. Only recommend retry for failures that specifically match a known issue; investigate the rest. | +| `LIKELY_PR_RELATED` | Failures correlate with PR changes. Lead with "fix these before retrying" and list `correlatedFiles`. | +| `POSSIBLY_TRANSIENT` | Failures could not be automatically classified — does NOT mean they are transient. Use `failedJobDetails` to investigate each failure individually. | +| `REVIEW_REQUIRED` | Could not auto-determine cause. Review failures manually. | +| `MERGE_CONFLICTS` | PR has merge conflicts — CI won't run. Tell the user to resolve conflicts. Offer to analyze a previous build by ID. | +| `NO_BUILDS` | No AzDO builds found (CI not triggered). Offer to check if CI needs to be triggered or analyze a previous build. | + +Then layer in nuance the heuristic can't capture: + +- **Mixed signals**: Some failures match known issues AND some correlate with PR changes → separate them. Known issues = safe to retry; correlated = fix first. +- **Canceled jobs with recoverable results**: If `canceledJobNames` is non-empty, mention that canceled jobs may have passing Helix results (see "Recovering Results from Crashed/Canceled Jobs"). +- **Build still in progress**: If `lastBuildJobSummary.pending > 0`, note that more failures may appear. +- **Multiple builds**: If `builds` has >1 entry, `lastBuildJobSummary` reflects only the last build — use `totalFailedJobs` for the aggregate count. +- **BuildId mode**: `knownIssues` and `prCorrelation` won't be populated. Say "Build Analysis and PR correlation not available in BuildId mode." + +### How to Retry + +- **AzDO builds**: Comment `/azp run {pipeline-name}` on the PR (e.g., `/azp run dotnet-sdk-public`) +- **All pipelines**: Comment `/azp run` to retry all failing pipelines +- **Helix work items**: Cannot be individually retried — must re-run the entire AzDO build + +### Tone and output format + +Be direct. Lead with the most important finding. Structure your response as: +1. **Summary verdict** (1-2 sentences) — Is CI green? Failures PR-related? Known issues? +2. **Failure details** (2-4 bullets) — what failed, why, evidence +3. **Recommended actions** (numbered) — retry, fix, investigate. Include `/azp run` commands. + +Synthesize from: JSON summary (structured facts) + human-readable output (details/logs) + Step 0 context (PR type, author intent). + +## Analysis Workflow + +### Step 0: Gather Context (before running anything) + +Before running the script, read the PR to understand what you're analyzing. Context changes how you interpret every failure. + +1. **Read PR metadata** — title, description, author, labels, linked issues +2. **Classify the PR type** — this determines your interpretation framework: + +| PR Type | How to detect | Interpretation shift | +|---------|--------------|---------------------| +| **Code PR** | Human author, code changes | Failures likely relate to the changes | +| **Flow/Codeflow PR** | Author is `dotnet-maestro[bot]`, title mentions "Update dependencies" | Missing packages may be behavioral, not infrastructure (see anti-pattern below) | +| **Backport** | Title mentions "backport", targets a release branch | Failures may be branch-specific; check if test exists on target branch | +| **Merge PR** | Merging between branches (e.g., release → main) | Conflicts and merge artifacts cause failures, not the individual changes | +| **Dependency update** | Bumps package versions, global.json changes | Build failures often trace to the dependency, not the PR's own code | + +3. **Check existing comments** — has someone already diagnosed the failures? Is there a retry pending? +4. **Note the changed files** — you'll use these to evaluate correlation after the script runs + +> ❌ **Don't skip Step 0.** Running the script without PR context leads to misdiagnosis — especially for flow PRs where "package not found" looks like infrastructure but is actually a code issue. + +### Step 1: Run the script + +Run with `-ShowLogs` for detailed failure info. + +### Step 2: Analyze results + +1. **Check Build Analysis** — If the Build Analysis GitHub check is **green**, all failures matched known issues and it's safe to retry. If it's **red**, some failures are unaccounted for — you must identify which failing jobs are covered by known issues and which are not. For 3+ failures, use SQL tracking to avoid missed matches (see [references/sql-tracking.md](references/sql-tracking.md)). +2. **Correlate with PR changes** — Same files failing = likely PR-related +3. **Compare with baseline** — If a test passes on the target branch but fails on the PR, compare Helix binlogs. See [references/binlog-comparison.md](references/binlog-comparison.md) — **delegate binlog download/extraction to subagents** to avoid burning context on mechanical work. +4. **Check build progression** — If the PR has multiple builds (multiple pushes), check whether earlier builds passed. A failure that appeared after a specific push narrows the investigation to those commits. See [references/build-progression-analysis.md](references/build-progression-analysis.md). Present findings as facts, not fix recommendations. +5. **Interpret patterns** (but don't jump to conclusions): + - Same error across many jobs → Real code issue + - Build Analysis flags a known issue → That *specific failure* is safe to retry (but others may not be) + - Failure is **not** in Build Analysis → Investigate further before assuming transient + - Device failures, Docker pulls, network timeouts → *Could* be infrastructure, but verify against the target branch first + - Test timeout but tests passed → Executor issue, not test failure +6. **Check for mismatch with user's question** — The script only reports builds for the current head SHA. If the user asks about a job, error, or cancellation that doesn't appear in the results, **ask** if they're referring to a prior build. Common triggers: + - User mentions a canceled job but `canceledJobNames` is empty + - User says "CI is failing" but the latest build is green + - User references a specific job name not in the current results + Offer to re-run with `-BuildId` if the user can provide the earlier build ID from AzDO. + +### Step 3: Verify before claiming + +Before stating a failure's cause, verify your claim: + +- **"Infrastructure failure"** → Did Build Analysis flag it? Does the same test pass on the target branch? If neither, don't call it infrastructure. +- **"Transient/flaky"** → Has it failed before? Is there a known issue? A single non-reproducing failure isn't enough to call it flaky. +- **"PR-related"** → Do the changed files actually relate to the failing test? Correlation in the script output is heuristic, not proof. +- **"Safe to retry"** → Are ALL failures accounted for (known issues or infrastructure), or are you ignoring some? Check the Build Analysis check status — if it's red, not all failures are matched. Map each failing job to a specific known issue before concluding "safe to retry." +- **"Not related to this PR"** → Have you checked if the test passes on the target branch? Don't assume — verify. + +## References + +- **Helix artifacts & binlogs**: See [references/helix-artifacts.md](references/helix-artifacts.md) +- **Binlog comparison (passing vs failing)**: See [references/binlog-comparison.md](references/binlog-comparison.md) +- **Build progression (commit-to-build correlation)**: See [references/build-progression-analysis.md](references/build-progression-analysis.md) +- **Subagent delegation patterns**: See [references/delegation-patterns.md](references/delegation-patterns.md) +- **Azure CLI deep investigation**: See [references/azure-cli.md](references/azure-cli.md) +- **Manual investigation steps**: See [references/manual-investigation.md](references/manual-investigation.md) +- **SQL tracking for investigations**: See [references/sql-tracking.md](references/sql-tracking.md) +- **AzDO/Helix details**: See [references/azdo-helix-reference.md](references/azdo-helix-reference.md) + +## Tips + +1. Check if same test fails on the target branch before assuming transient +2. Look for `[ActiveIssue]` attributes for known skipped tests +3. Use `-SearchMihuBot` for semantic search of related issues +4. Use binlog analysis tools to search binlogs for Helix job IDs, build errors, and properties +5. `gh pr checks --json` valid fields: `bucket`, `completedAt`, `description`, `event`, `link`, `name`, `startedAt`, `state`, `workflow` — no `conclusion` field, `state` has `SUCCESS`/`FAILURE` directly +6. "Canceled" ≠ "Failed" — canceled jobs may have recoverable Helix results. Check artifacts before concluding results are lost. diff --git a/.github/skills/ci-analysis/references/azdo-helix-reference.md b/.github/skills/ci-analysis/references/azdo-helix-reference.md new file mode 100644 index 000000000000..ace39f0932ee --- /dev/null +++ b/.github/skills/ci-analysis/references/azdo-helix-reference.md @@ -0,0 +1,93 @@ +# Azure DevOps and Helix Reference + +## Supported Repositories + +The script works with any dotnet repository that uses Azure DevOps and Helix: + +| Repository | Common Pipelines | +|------------|-----------------| +| `dotnet/runtime` | runtime, runtime-dev-innerloop, dotnet-linker-tests | +| `dotnet/sdk` | dotnet-sdk (mix of local and Helix tests) | +| `dotnet/aspnetcore` | aspnetcore-ci | +| `dotnet/roslyn` | roslyn-CI | +| `dotnet/maui` | maui-public | + +Use `-Repository` to specify the target: +```powershell +./scripts/Get-CIStatus.ps1 -PRNumber 12345 -Repository "dotnet/aspnetcore" +``` + +## Build Definition IDs (Example: dotnet/runtime) + +Each repository has its own build definition IDs. Here are common ones for dotnet/runtime: + +| Definition ID | Name | Description | +|---------------|------|-------------| +| `129` | runtime | Main PR validation build | +| `133` | runtime-dev-innerloop | Fast innerloop validation | +| `139` | dotnet-linker-tests | ILLinker/trimming tests | + +**Note:** The script auto-discovers builds for a PR, so you rarely need to know definition IDs. + +## Azure DevOps Organizations + +**Public builds (default):** +- Organization: `dnceng-public` +- Project: `cbb18261-c48f-4abb-8651-8cdcb5474649` + +**Internal/private builds:** +- Organization: `dnceng` +- Project GUID: Varies by pipeline + +Override with: +```powershell +./scripts/Get-CIStatus.ps1 -BuildId 1276327 -Organization "dnceng" -Project "internal-project-guid" +``` + +## Common Pipeline Names (Example: dotnet/runtime) + +| Pipeline | Description | +|----------|-------------| +| `runtime` | Main PR validation build | +| `runtime-dev-innerloop` | Fast innerloop validation | +| `dotnet-linker-tests` | ILLinker/trimming tests | +| `runtime-wasm-perf` | WASM performance tests | +| `runtime-libraries enterprise-linux` | Enterprise Linux compatibility | + +Other repos have different pipelines - the script discovers them automatically from the PR. + +## Useful Links + +- [Helix Portal](https://helix.dot.net/): View Helix jobs and work items (all repos) +- [Helix API Documentation](https://helix.dot.net/swagger/): Swagger docs for Helix REST API +- [Build Analysis](https://github.com/dotnet/arcade/blob/main/Documentation/Projects/Build%20Analysis/LandingPage.md): Known issues tracking (arcade infrastructure) +- [dnceng-public AzDO](https://dev.azure.com/dnceng-public/public/_build): Public builds for all dotnet repos + +### Repository-specific docs: +- [runtime: Triaging Failures](https://github.com/dotnet/runtime/blob/main/docs/workflow/ci/triaging-failures.md) +- [runtime: Area Owners](https://github.com/dotnet/runtime/blob/main/docs/area-owners.md) + +## Test Execution Types + +### Helix Tests +Tests run on Helix distributed test infrastructure. The script extracts console log URLs and can fetch detailed failure info with `-ShowLogs`. + +### Local Tests (Non-Helix) +Some repositories (e.g., dotnet/sdk) run tests directly on the build agent. The script detects these and extracts Azure DevOps Test Run URLs. + +## Known Issue Labels + +- `Known Build Error` - Used by Build Analysis across all dotnet repositories +- Search syntax: `repo:/ is:issue is:open label:"Known Build Error" ` + +Example searches (use `search_issues` when GitHub MCP is available, `gh` CLI otherwise): +```bash +# Search in runtime +gh issue list --repo dotnet/runtime --label "Known Build Error" --search "FileSystemWatcher" + +# Search in aspnetcore +gh issue list --repo dotnet/aspnetcore --label "Known Build Error" --search "Blazor" + +# Search in sdk +gh issue list --repo dotnet/sdk --label "Known Build Error" --search "template" +``` diff --git a/.github/skills/ci-analysis/references/azure-cli.md b/.github/skills/ci-analysis/references/azure-cli.md new file mode 100644 index 000000000000..b0371fb0fdac --- /dev/null +++ b/.github/skills/ci-analysis/references/azure-cli.md @@ -0,0 +1,96 @@ +# Deep Investigation with Azure CLI + +The AzDO MCP tools handle most pipeline queries directly. This reference covers the Azure CLI fallback for cases where MCP tools are unavailable or the endpoint isn't exposed (e.g., downloading artifacts, inspecting pipeline definitions). + +When the CI script and GitHub APIs aren't enough (e.g., investigating internal pipeline definitions or downloading build artifacts), use the Azure CLI with the `azure-devops` extension. + +> 💡 **Prefer `az pipelines` / `az devops` commands over raw REST API calls.** The CLI handles authentication, pagination, and JSON output formatting. Only fall back to manual `Invoke-RestMethod` calls when the CLI doesn't expose the endpoint you need (e.g., build timelines). The CLI's `--query` (JMESPath) and `-o table` flags are powerful for filtering without extra scripting. + +## Checking Authentication + +Before making AzDO API calls, verify the CLI is installed and authenticated: + +```powershell +# Ensure az is on PATH (Windows may need a refresh after install) +$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") + +# Check if az CLI is available +az --version 2>$null | Select-Object -First 1 + +# Check if logged in and get current account +az account show --query "{name:name, user:user.name}" -o table 2>$null + +# If not logged in, prompt the user to authenticate: +# az login # Interactive browser login +# az login --use-device-code # Device code flow (for remote/headless) + +# Get an AAD access token for AzDO REST API calls (only needed for raw REST) +$accessToken = (az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv) +$headers = @{ "Authorization" = "Bearer $accessToken" } +``` + +> ⚠️ If `az` is not installed, use `winget install -e --id Microsoft.AzureCLI` (Windows). The `azure-devops` extension is also required — install or verify it with `az extension add --name azure-devops` (safe to run if already installed). Ask the user to authenticate if needed. + +> ⚠️ **Do NOT use `az devops configure --defaults`** — it sets user-wide defaults that may not match the organization/project needed for dotnet repositories. Always pass `--org` and `--project` (or `-p`) explicitly on each command. + +## Querying Pipeline Definitions and Builds + +```powershell +$org = "https://dev.azure.com/dnceng" +$project = "internal" + +# Find a pipeline definition by name +az pipelines list --name "dotnet-unified-build" --org $org -p $project --query "[].{id:id, name:name, path:path}" -o table + +# Get pipeline definition details (shows YAML path, triggers, etc.) +az pipelines show --id 1330 --org $org -p $project --query "{id:id, name:name, yamlPath:process.yamlFilename, repo:repository.name}" -o table + +# List recent builds for a pipeline (replace {TARGET_BRANCH} with the PR's base branch, e.g., main or release/9.0) +az pipelines runs list --pipeline-ids 1330 --branch "refs/heads/{TARGET_BRANCH}" --top 5 --org $org -p $project --query "[].{id:id, result:result, finish:finishTime}" -o table + +# Get a specific build's details +az pipelines runs show --id $buildId --org $org -p $project --query "{id:id, result:result, sourceBranch:sourceBranch}" -o table + +# List build artifacts +az pipelines runs artifact list --run-id $buildId --org $org -p $project --query "[].{name:name, type:resource.type}" -o table + +# Download a build artifact +az pipelines runs artifact download --run-id $buildId --artifact-name "TestBuild_linux_x64" --path "$env:TEMP\artifact" --org $org -p $project +``` + +## REST API Fallback + +Fall back to REST API only when the CLI doesn't expose what you need: + +```powershell +# Get build timeline (stages, jobs, tasks with results and durations) — no CLI equivalent +$accessToken = (az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv) +$headers = @{ "Authorization" = "Bearer $accessToken" } +$timelineUrl = "https://dev.azure.com/dnceng/internal/_apis/build/builds/$buildId/timeline?api-version=7.1" +$timeline = (Invoke-RestMethod -Uri $timelineUrl -Headers $headers) +$timeline.records | Where-Object { $_.result -eq "failed" -and $_.type -eq "Job" } +``` + +## Examining Pipeline YAML + +All dotnet repos that use arcade put their pipeline definitions under `eng/pipelines/`. Use `az pipelines show` to find the YAML file path, then fetch it: + +```powershell +# Find the YAML path for a pipeline +az pipelines show --id 1330 --org $org -p $project --query "{yamlPath:process.yamlFilename, repo:repository.name}" -o table + +# Fetch the YAML from the repo (example: dotnet/runtime's runtime-official pipeline) +# Read the pipeline YAML from the repo to understand build stages and conditions +# e.g., eng/pipelines/runtime-official.yml in dotnet/runtime + +# For VMR unified builds, the YAML is in dotnet/dotnet: +# eng/pipelines/unified-build.yml + +# Templates are usually in eng/pipelines/common/ or eng/pipelines/templates/ +``` + +This is especially useful when: +- A job name doesn't clearly indicate what it builds +- You need to understand stage dependencies (why a job was canceled) +- You want to find which template defines a specific step +- Investigating whether a pipeline change caused new failures diff --git a/.github/skills/ci-analysis/references/binlog-comparison.md b/.github/skills/ci-analysis/references/binlog-comparison.md new file mode 100644 index 000000000000..8cbd2a6a20ca --- /dev/null +++ b/.github/skills/ci-analysis/references/binlog-comparison.md @@ -0,0 +1,119 @@ +# Deep Investigation: Binlog Comparison + +When a test **passes on the target branch but fails on a PR**, comparing MSBuild binlogs from both runs reveals the exact difference in task parameters without guessing. + +## When to Use This Pattern + +- Test assertion compares "expected vs actual" build outputs (e.g., CSC args, reference lists) +- A build succeeds on one branch but fails on another with different MSBuild behavior +- You need to find which MSBuild property/item change caused a specific task to behave differently + +## The Pattern: Delegate to Subagents + +> ⚠️ **Do NOT download, load, and parse binlogs in the main conversation context.** This burns 10+ turns on mechanical work. Delegate to subagents instead. + +### Step 1: Identify the two work items to compare + +Use `Get-CIStatus.ps1` to find the failing Helix job + work item, then find a corresponding passing build (recent PR merged to the target branch, or a CI run on that branch). + +**Finding Helix job IDs from build artifacts (binlogs to find binlogs):** +When the failing work item's Helix job ID isn't visible (e.g., canceled jobs, or finding a matching job from a passing build), the IDs are inside the build's `SendToHelix.binlog`: + +1. Download the build artifact with `az`: + ``` + az pipelines runs artifact list --run-id $buildId --org "https://dev.azure.com/dnceng-public" -p public --query "[].name" -o tsv + az pipelines runs artifact download --run-id $buildId --artifact-name "TestBuild_linux_x64" --path "$env:TEMP\artifact" --org "https://dev.azure.com/dnceng-public" -p public + ``` +2. Load the `SendToHelix.binlog` and search for `Sent Helix Job` to find the GUIDs. +3. Query each Helix job GUID with the CI script: + ``` + ./scripts/Get-CIStatus.ps1 -HelixJob "{GUID}" -FindBinlogs + ``` + +**For Helix work item binlogs (the common case):** +The CI script shows binlog URLs directly when you query a specific work item: +``` +./scripts/Get-CIStatus.ps1 -HelixJob "{JOB_ID}" -WorkItem "{WORK_ITEM}" +# Output includes: 🔬 msbuild.binlog: https://helix...blob.core.windows.net/... +``` + +### Step 2: Dispatch parallel subagents for extraction + +Launch two `task` subagents (can run in parallel), each with a prompt like: + +``` +Download the msbuild.binlog from Helix job {JOB_ID} work item {WORK_ITEM}. +Use the CI skill script to get the artifact URL: + ./scripts/Get-CIStatus.ps1 -HelixJob "{JOB_ID}" -WorkItem "{WORK_ITEM}" +Download the binlog, load it, find the {TASK_NAME} task, and extract CommandLineArguments. +Normalize paths (see table below) and sort args. +Parse into individual args using regex: (?:"[^"]+"|/[^\s]+|[^\s]+) +Report the total arg count prominently. +``` + +**Important:** When diffing, look for **extra or missing args** (different count), not value differences in existing args. A Debug/Release difference in `/define:` is expected noise — an extra `/analyzerconfig:` or `/reference:` arg is the real signal. + +### Step 3: Diff the results + +With two normalized arg lists, `Compare-Object` instantly reveals the difference. + +## Common Binlog Search Patterns + +When investigating binlogs, these search query patterns are most useful: + +- Search for a property: `analysislevel` +- Search within a target: `under($target AddGlobalAnalyzerConfigForPackage_MicrosoftCodeAnalysisNetAnalyzers)` +- Find all properties matching a pattern: `GlobalAnalyzerConfig` + +## Path Normalization + +Helix work items run on different machines with different paths. Normalize before comparing: + +| Pattern | Replacement | Example | +|---------|-------------|---------| +| `/datadisks/disk1/work/[A-F0-9]{8}` | `{W}` | Helix work directory (Linux) | +| `C:\h\w\[A-F0-9]{8}` | `{W}` | Helix work directory (Windows) | +| `Program-[a-f0-9]{64}` | `Program-{H}` | Runfile content hash | +| `dotnetSdkTests\.[a-zA-Z0-9]+` | `dotnetSdkTests.{T}` | Temp test directory | + +### After normalizing paths, focus on structural differences + +> ⚠️ **Ignore value-only differences in existing args** (e.g., Debug vs Release in `/define:`, different hash paths). These are expected configuration differences. Focus on **extra or missing args** — a different arg count indicates a real build behavior change. + +## Example: CscArguments Investigation + +A merge PR (release/10.0.3xx → main) had 208 CSC args vs 207 on main. The diff: + +``` +FAIL-ONLY: /analyzerconfig:{W}/p/d/sdk/11.0.100-ci/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_11_default.globalconfig +``` + +### What the binlog properties showed + +Both builds had identical property resolution: +- `EffectiveAnalysisLevel = 11.0` +- `_GlobalAnalyzerConfigFileName = analysislevel_11_default.globalconfig` +- `_GlobalAnalyzerConfigFile = .../config/analysislevel_11_default.globalconfig` + +### The actual root cause + +The `AddGlobalAnalyzerConfigForPackage` target has an `Exists()` condition: +```xml + + + +``` + +The merge's SDK layout **shipped** `analysislevel_11_default.globalconfig` on disk (from a newer roslyn-analyzers that flowed from 10.0.3xx), while main's SDK didn't have that file yet. Same property values, different files on disk = different build behavior. + +### Lesson learned + +Same MSBuild property resolution + different files on disk = different build behavior. Always check what's actually in the SDK layout, not just what the targets compute. + +## Anti-Patterns + +> ❌ **Don't manually split/parse CSC command lines in the main conversation.** CSC args have quoted paths, spaces, and complex structure. Regex parsing in PowerShell is fragile and burns turns on trial-and-error. Use a subagent. + +> ❌ **Don't assume the MSBuild property diff explains the behavior diff.** Two branches can compute identical property values but produce different outputs because of different files on disk, different NuGet packages, or different task assemblies. Compare the actual task invocation. + +> ❌ **Don't load large binlogs and browse them interactively in main context.** Use targeted searches rather than browsing interactively. Get in, get the data, get out. diff --git a/.github/skills/ci-analysis/references/build-progression-analysis.md b/.github/skills/ci-analysis/references/build-progression-analysis.md new file mode 100644 index 000000000000..5d13b819bb36 --- /dev/null +++ b/.github/skills/ci-analysis/references/build-progression-analysis.md @@ -0,0 +1,219 @@ +# Deep Investigation: Build Progression Analysis + +When the current build is failing, the PR's build history can reveal whether the failure existed from the start or appeared after specific changes. This is a fact-gathering technique — like target-branch comparison — that provides context for understanding the current failure. + +## When to Use This Pattern + +- Standard analysis (script + logs) hasn't identified the root cause of the current failure +- The PR has multiple pushes and you want to know whether earlier builds passed or failed +- You need to understand whether a failure is inherent to the PR's approach or was introduced by a later change + +## The Pattern + +### Step 0: Start with the recent builds + +Don't try to analyze the full build history upfront — especially on large PRs with many pushes. Start with the most recent N builds (5-8), present the progression table, and let the user decide whether to dig deeper into earlier builds. + +On large PRs, the user is usually iterating toward a solution. The recent builds are the most relevant. Offer: "Here are the last N builds — the pass→fail transition was between X and Y. Want me to look at earlier builds?" + +### Step 1: List builds for the PR + +`gh pr checks` only shows checks for the current HEAD SHA. To see the full build history, use AzDO or CLI: + +**With AzDO (preferred):** + +Query AzDO for builds on `refs/pull/{PR}/merge` branch, sorted by queue time descending, top 20, in the `public` project. The response includes `triggerInfo` with `pr.sourceSha` — the PR's HEAD commit for each build. + +> 💡 Key parameters: `branchName: "refs/pull/{PR}/merge"`, `queryOrder: "QueueTimeDescending"`, `top: 20`, project `public` (for dnceng-public org). + +**Without MCP (fallback):** +```powershell +$org = "https://dev.azure.com/dnceng-public" +$project = "public" +az pipelines runs list --branch "refs/pull/{PR}/merge" --top 20 --org $org -p $project -o json +``` + +### Step 2: Map builds to the PR's head commit + +Each build's `triggerInfo` contains `pr.sourceSha` — the PR's HEAD commit when the build was triggered. Extract it from the build response or CLI output. + +> ⚠️ **`sourceVersion` is the merge commit**, not the PR's head commit. Use `triggerInfo.'pr.sourceSha'` instead. + +> ⚠️ **Target branch moves between builds.** Each build merges `pr.sourceSha` into the target branch HEAD *at the time the build starts*. If `main` received new commits between build N and N+1, the two builds merged against different baselines — even if `pr.sourceSha` is the same. Always extract the target branch HEAD to detect baseline shifts. + +### Step 2b: Extract the target branch HEAD + +**Shortcut for the latest build — use the GitHub merge commit:** + +For the current/latest build, the merge ref (`refs/pull/{PR}/merge`) is available via the GitHub API. The merge commit's first parent is the target branch HEAD at the time GitHub computed the merge: + +Look up the merge commit's parents — the first parent is the target branch HEAD. Use the GitHub API or MCP (`get_commit` with the `sourceVersion` SHA) to get the commit details. The `sourceVersion` from the AzDO build is the merge commit SHA (not `pr.sourceSha`). Example: + +``` +gh api repos/{owner}/{repo}/git/commits/{sourceVersion} --jq '.parents[0].sha' +``` + +This is simpler than parsing checkout logs. + +> ⚠️ **This only works for the latest build.** GitHub recomputes `refs/pull/{PR}/merge` on each push, so the merge commit changes. For historical builds in a progression analysis, the merge ref no longer reflects what was built — use the checkout log method below. + +**For historical builds — extract from checkout logs:** + +The AzDO build API doesn't expose the target branch SHA. Extract it from the checkout task log. + +**With AzDO (preferred):** + +Fetch the checkout task log for the build — typically **log ID 5**, starting around **line 500+** (skip the early git-fetch output). Search the output for the merge line: +``` +HEAD is now at {mergeCommit} Merge {prSourceSha} into {targetBranchHead} +``` + +> 💡 `logId: 5` is the first checkout task in most dotnet pipelines. If it doesn't contain the merge line, check the build timeline for "Checkout" tasks to find the correct log ID. + +**Without MCP (fallback):** +```powershell +$token = az account get-access-token --resource "499b84ac-1321-427f-aa17-267ca6975798" --query accessToken -o tsv +$headers = @{ Authorization = "Bearer $token" } +$logUrl = "https://dev.azure.com/{org}/{project}/_apis/build/builds/{BUILD_ID}/logs/5" +$log = Invoke-RestMethod -Uri $logUrl -Headers $headers +``` + +> Note: log ID 5 is the first checkout task in most pipelines. The merge line is typically around line 500-650. If log 5 doesn't contain it, check the build timeline for "Checkout" tasks. + +Note: a PR may have more unique `pr.sourceSha` values than commits visible on GitHub, because force-pushes replace the commit history. Each force-push triggers a new build with a new merge commit and a new `pr.sourceSha`. + +### Step 3: Store progression in SQL + +Use the SQL tool to track builds as you discover them. This avoids losing context and enables queries across the full history: + +```sql +CREATE TABLE IF NOT EXISTS build_progression ( + build_id INT PRIMARY KEY, + pr_sha TEXT, + target_sha TEXT, + result TEXT, -- passed, failed, canceled + queued_at TEXT, + failed_jobs TEXT, -- comma-separated job names + notes TEXT +); +``` + +Insert rows as you extract data from each build: + +```sql +INSERT INTO build_progression VALUES + (1283986, '7af79ad', '2d638dc', 'failed', '2026-02-08T10:00:00Z', 'WasmBuildTests', 'Initial commits'), + (1284169, '28ec8a0', '0b691ba', 'failed', '2026-02-08T14:00:00Z', 'WasmBuildTests', 'Iteration 2'), + (1284433, '39dc0a6', '18a3069', 'passed', '2026-02-09T09:00:00Z', NULL, 'Iteration 3'); +``` + +Then query to find the pass→fail transition: + +```sql +-- Find where it went from passing to failing +SELECT * FROM build_progression ORDER BY queued_at; + +-- Did the target branch move between pass and fail? +SELECT pr_sha, target_sha, result FROM build_progression +WHERE result IN ('passed', 'failed') ORDER BY queued_at; + +-- Which builds share the same PR SHA? (force-push detection) +SELECT pr_sha, COUNT(*) as builds, GROUP_CONCAT(result) as results +FROM build_progression GROUP BY pr_sha HAVING builds > 1; +``` + +Present the table to the user: + +| PR HEAD | Target HEAD | Builds | Result | Notes | +|---------|-------------|--------|--------|-------| +| 7af79ad | 2d638dc | 1283986 | ❌ | Initial commits | +| 28ec8a0 | 0b691ba | 1284169 | ❌ | Iteration 2 | +| 39dc0a6 | 18a3069 | 1284433 | ✅ | Iteration 3 | +| f186b93 | 5709f35 | 1286087 | ❌ | Added commit C; target moved ~35 commits | +| 2e74845 | 482d8f9 | 1286967 | ❌ | Modified commit C | + +When both `pr.sourceSha` AND `Target HEAD` change between a pass→fail transition, either could be the cause. Analyze the failure content to determine which. If only the target moved (same `pr.sourceSha`), the failure came from the new baseline. + +#### Tracking individual test failures across builds + +For deeper analysis, track which tests failed in each build: + +```sql +CREATE TABLE IF NOT EXISTS build_failures ( + build_id INT, + job_name TEXT, + test_name TEXT, + error_snippet TEXT, + helix_job TEXT, + work_item TEXT, + PRIMARY KEY (build_id, job_name, test_name) +); +``` + +Insert failures as you investigate each build, then query for patterns: + +```sql +-- Tests that fail in every build (persistent, not flaky) +SELECT test_name, COUNT(DISTINCT build_id) as fail_count, GROUP_CONCAT(build_id) as builds +FROM build_failures GROUP BY test_name HAVING fail_count > 1; + +-- New failures in the latest build (what changed?) +SELECT f.* FROM build_failures f +LEFT JOIN build_failures prev ON f.test_name = prev.test_name AND prev.build_id = {PREV_BUILD_ID} +WHERE f.build_id = {LATEST_BUILD_ID} AND prev.test_name IS NULL; + +-- Flaky tests: fail in some builds, pass in others +SELECT test_name FROM build_failures GROUP BY test_name +HAVING COUNT(DISTINCT build_id) < (SELECT COUNT(*) FROM build_progression WHERE result = 'failed'); +``` + +### Step 4: Present findings, not conclusions + +Report what the progression shows: +- Which builds passed and which failed +- What commits were added between the last passing and first failing build +- Whether the failing commits were added in response to review feedback (check review threads) + +> 💡 **Stop when you have the progression table and the pass→fail transition identified.** The table + transition commits + error category is enough for the user to act. Don't investigate further (e.g., comparing individual commits, checking passing builds, exploring main branch history) unless the user asks. + +**Do not** make fix recommendations based solely on build progression. The progression narrows the investigation — it doesn't determine the right fix. The human may have context about why changes were made, what constraints exist, or what the reviewer intended. + +## Checking review context + +When the progression shows that a failure appeared after new commits, check whether those commits were review-requested: + +```powershell +# Get review comments with timestamps +gh api "repos/{OWNER}/{REPO}/pulls/{PR}/comments" ` + --jq '.[] | {author: .user.login, body: .body, created: .created_at}' +``` + +Present this as additional context: "Commit C was pushed after reviewer X commented requesting Y." Let the author decide how to proceed. + +## Combining with Binlog Comparison + +Build progression identifies **which change** correlates with the current failure. Binlog comparison (see [binlog-comparison.md](binlog-comparison.md)) shows **what's different** in the build between a passing and failing state. Together they provide a complete picture: + +1. Progression → "The current failure first appeared in build N+1, which added commit C" +2. Binlog comparison → "In the current (failing) build, task X receives parameter Y=Z, whereas in the passing build it received Y=W" + +## Relationship to Target-Branch Comparison + +Both techniques compare a failing build against a passing one: + +| Technique | Passing build from | Answers | +|-----------|-------------------|---------| +| **Target-branch comparison** | Recent build on the base branch (e.g., main) | "Does this test pass without the PR's changes at all?" | +| **Build progression** | Earlier build on the same PR | "Did this test pass with the PR's *earlier* changes?" | + +Use target-branch comparison first to confirm the failure is PR-related. Use build progression to narrow down *which part* of the PR introduced it. If build progression shows a pass→fail transition with the same `pr.sourceSha`, the target branch is the more likely culprit — use target-branch comparison to confirm. + +## Anti-Patterns + +> ❌ **Don't treat build history as a substitute for analyzing the current build.** The current build determines CI status. Build history is context for understanding and investigating the current failure. + +> ❌ **Don't make fix recommendations from progression alone.** "Build N passed and build N+1 failed after adding commit C" is a fact worth reporting. "Therefore revert commit C" is a judgment that requires more context than the agent has — the commit may be addressing a critical review concern, fixing a different bug, or partially correct. + +> ❌ **Don't assume earlier passing builds prove the original approach was complete.** A build may pass because it didn't change enough to trigger the failing test scenario. The reviewer who requested additional changes may have identified a real gap. + +> ❌ **Don't assume MSBuild changes only affect the platform you're looking at.** MSBuild properties, conditions, and targets are shared infrastructure. A commit that changes a condition, moves a property, or modifies a restore flag can impact any platform that evaluates the same code path. When a commit touches MSBuild files, verify its impact across all platforms — don't assume it's scoped to the one you're investigating. diff --git a/.github/skills/ci-analysis/references/delegation-patterns.md b/.github/skills/ci-analysis/references/delegation-patterns.md new file mode 100644 index 000000000000..b7dd706fca94 --- /dev/null +++ b/.github/skills/ci-analysis/references/delegation-patterns.md @@ -0,0 +1,124 @@ +# Subagent Delegation Patterns + +CI investigations involve repetitive, mechanical work that burns main conversation context. Delegate data gathering to subagents; keep interpretation in the main agent. + +## Pattern 1: Scanning Multiple Console Logs + +**When:** Multiple failing work items across several jobs. + +**Delegate:** +``` +Extract all unique test failures from these Helix work items: + +Job: {JOB_ID_1}, Work items: {ITEM_1}, {ITEM_2} +Job: {JOB_ID_2}, Work items: {ITEM_3} + +For each, search console logs for lines ending with [FAIL] (xUnit format). +If hlx MCP is not available, fall back to: + ./scripts/Get-CIStatus.ps1 -HelixJob "{JOB}" -WorkItem "{ITEM}" + +Extract lines ending with [FAIL] (xUnit format). Ignore [OUTPUT] and [PASS] lines. + +Return JSON: { "failures": [{ "test": "Namespace.Class.Method", "workItems": ["item1", "item2"] }] } +``` + +## Pattern 2: Finding a Baseline Build + +**When:** A test fails on a PR — need to confirm it passes on the target branch. + +**Delegate:** +``` +Find a recent passing build on {TARGET_BRANCH} of dotnet/{REPO} that ran the same test leg. + +Failing build: {BUILD_ID}, job: {JOB_NAME}, work item: {WORK_ITEM} + +Steps: +1. Search for recently merged PRs: + Search for recently merged PRs on {TARGET_BRANCH} +2. Run: ./scripts/Get-CIStatus.ps1 -PRNumber {MERGED_PR} -Repository "dotnet/{REPO}" +3. Find the build with same job name that passed +4. Locate the Helix job ID (may need artifact download — see [azure-cli.md](azure-cli.md)) + +Return JSON: { "found": true, "buildId": N, "helixJob": "...", "workItem": "...", "result": "Pass" } +Or: { "found": false, "reason": "no passing build in last 5 merged PRs" } + +If authentication fails or API returns errors, STOP and return the error — don't troubleshoot. +``` + +## Pattern 3: Extracting Merge PR Changed Files + +**When:** A large merge PR (hundreds of files) has test failures — need the file list for the main agent to analyze. + +**Delegate:** +``` +List all changed files on merge PR #{PR_NUMBER} in dotnet/{REPO}. + +Get the list of changed files for PR #{PR_NUMBER} in dotnet/{REPO} + +For each file, note: path, change type (added/modified/deleted), lines changed. + +Return JSON: { "totalFiles": N, "files": [{ "path": "...", "changeType": "modified", "linesChanged": N }] } +``` + +> The main agent decides which files are relevant to the specific failures — don't filter in the subagent. + +## Pattern 4: Parallel Artifact Extraction + +**When:** Multiple builds or artifacts need independent analysis — binlog comparison, canceled job recovery, multi-build progression. + +**Key insight:** Launch one subagent per build/artifact in parallel. Each does its mechanical extraction independently. The main agent synthesizes results across all of them. + +**Delegate (per build, for binlog analysis):** +``` +Download and analyze binlog from AzDO build {BUILD_ID}, artifact {ARTIFACT_NAME}. + +Steps: +1. Download the artifact (see [azure-cli.md](azure-cli.md)) +2. Load the binlog, find the {TASK_NAME} task invocations, get full task details including CommandLineArguments. + +Return JSON: { "buildId": N, "project": "...", "args": ["..."] } +``` + +**Delegate (per build, for canceled job recovery):** +``` +Check if canceled job "{JOB_NAME}" from build {BUILD_ID} has recoverable Helix results. + +Steps: +1. Check if TRX test results are available for the work item. Parse them for pass/fail counts. +2. If no structured results, check for testResults.xml +3. Parse the XML for pass/fail counts on the element + +Return JSON: { "jobName": "...", "hasResults": true, "passed": N, "failed": N } +Or: { "jobName": "...", "hasResults": false, "reason": "no testResults.xml uploaded" } +``` + +This pattern scales to any number of builds — launch N subagents for N builds, collect results, compare. + +## Pattern 5: Build Progression with Target HEAD Extraction + +**When:** PR has multiple builds and you need the full progression table with target branch HEADs. + +**Delegate (one subagent per build):** +``` +Extract the target branch HEAD from AzDO build {BUILD_ID}. + +Fetch the checkout task log (typically LOG ID 5, starting around LINE 500+ to skip git-fetch output) + +Search for: "HEAD is now at {mergeCommit} Merge {prSourceSha} into {targetBranchHead}" + +Return JSON: { "buildId": N, "targetHead": "abc1234", "mergeCommit": "def5678" } +Or: { "buildId": N, "targetHead": null, "error": "merge line not found in log 5" } +``` + +Launch one per build in parallel. The main agent combines with the build list to build the full progression table. + +## General Guidelines + +- **Use `general-purpose` agent type** — it has shell + MCP access for Helix, AzDO, binlog, and GitHub queries +- **Run independent tasks in parallel** — the whole point of delegation +- **Include script paths** — subagents don't inherit skill context +- **Require structured JSON output** — enables comparison across subagents +- **Don't delegate interpretation** — subagents return facts, main agent reasons +- **STOP on errors** — subagents should return error details immediately, not troubleshoot auth/environment issues +- **Use SQL for many results** — when launching 5+ subagents or doing multi-phase delegation, store results in a SQL table (`CREATE TABLE results (agent_id TEXT, build_id INT, data TEXT, status TEXT)`) so you can query across all results instead of holding them in context +- **Specify `model: "claude-sonnet-4"` for MCP-heavy tasks** — default model may time out on multi-step MCP tool chains diff --git a/.github/skills/ci-analysis/references/helix-artifacts.md b/.github/skills/ci-analysis/references/helix-artifacts.md new file mode 100644 index 000000000000..756ea26ef5ef --- /dev/null +++ b/.github/skills/ci-analysis/references/helix-artifacts.md @@ -0,0 +1,285 @@ +# Helix Work Item Artifacts + +Guide to finding and analyzing artifacts from Helix test runs. + +## Accessing Artifacts + +### Via the Script + +Query a specific work item to see its artifacts: + +```powershell +./scripts/Get-CIStatus.ps1 -HelixJob "4b24b2c2-..." -WorkItem "Microsoft.NET.Sdk.Tests.dll.1" -ShowLogs +``` + +### Via API + +```bash +# Get work item details including Files array +curl -s "https://helix.dot.net/api/2019-06-17/jobs/{jobId}/workitems/{workItemName}" +``` + +The `Files` array contains artifacts with `FileName` and `Uri` properties. + +## Artifact Availability Varies + +**Not all test types produce the same artifacts.** What you see depends on the repo, test type, and configuration: + +- **Build/publish tests** (SDK, WASM) → Multiple binlogs +- **AOT compilation tests** (iOS/Android) → `AOTBuild.binlog` plus device logs +- **Standard unit tests** → Console logs only, no binlogs +- **Crash failures** (exit code 134) → Core dumps may be present + +Always query the specific work item to see what's available rather than assuming a fixed structure. + +## Common Artifact Patterns + +| File Pattern | Purpose | When Useful | +|--------------|---------|-------------| +| `*.binlog` | MSBuild binary logs | AOT/build failures, MSB4018 errors | +| `console.*.log` | Console output | Always available, general output | +| `run-*.log` | XHarness execution logs | Mobile test failures | +| `device-*.log` | Device-specific logs | iOS/Android device issues | +| `dotnetTestLog.*.log` | dotnet test output | Test framework issues | +| `vstest.*.log` | VSTest output | aspnetcore/SDK test issues | +| `core.*`, `*.dmp` | Core dumps | Crashes, hangs | +| `testResults.xml` | Test results | Detailed pass/fail info | + +Artifacts may be at the root level or nested in subdirectories like `xharness-output/logs/`. + +> **Note:** The Helix work item Details API has a known bug ([dotnet/dnceng#6072](https://github.com/dotnet/dnceng/issues/6072)) where +> file URIs for subdirectory files are incorrect, and unicode characters in filenames are rejected. +> The script works around this by using the separate `ListFiles` endpoint (`GET .../workitems/{workItemName}/files`) +> which returns direct blob storage URIs that work for all filenames regardless of subdirectories or unicode. + +## Binlog Files + +Binlogs are **only present for tests that invoke MSBuild** (build/publish tests, AOT compilation). Standard unit tests don't produce binlogs. + +### Common Names + +| File | Description | +|------|-------------| +| `build.msbuild.binlog` | Build phase | +| `publish.msbuild.binlog` | Publish phase | +| `AOTBuild.binlog` | AOT compilation | +| `msbuild.binlog` | General MSBuild operations | +| `msbuild0.binlog`, `msbuild1.binlog` | Per-test-run logs (numbered) | + +### Analyzing Binlogs + +**Online viewer (no download):** +1. Copy the binlog URI from the script output +2. Go to https://live.msbuildlog.com/ +3. Paste the URL to load and analyze + +**Download and view locally:** +```bash +curl -o build.binlog "https://helix.dot.net/api/jobs/{jobId}/workitems/{workItem}/files/build.msbuild.binlog?api-version=2019-06-17" +# Open with MSBuild Structured Log Viewer +``` + +**AI-assisted analysis:** +Use the MSBuild MCP server to analyze binlogs for errors and warnings. + +## Core Dumps + +Core dumps appear when tests crash (typically exit code 134 on Linux/macOS): + +``` +core.1000.34 # Format: core.{uid}.{pid} +``` + +## Mobile Test Artifacts (iOS/Android) + +Mobile device tests typically include XHarness orchestration logs: + +- `run-ios-device.log` / `run-android.log` - Execution log +- `device-{machine}-*.log` - Device output +- `list-ios-device-*.log` - Device discovery +- `AOTBuild.binlog` - AOT compilation (when applicable) +- `*.crash` - iOS crash reports + +## Finding the Right Work Item + +1. Run the script with `-ShowLogs` to see Helix job/work item info +2. Look for lines like: + ``` + Helix Job: 4b24b2c2-ad5a-4c46-8a84-844be03b1d51 + Work Item: Microsoft.NET.Sdk.Tests.dll.1 + ``` +3. Query that specific work item for full artifact list + +## AzDO Build Artifacts (Pre-Helix) + +Helix work items contain artifacts from **test execution**. But there's another source of binlogs: **AzDO build artifacts** from the build phase before tests are sent to Helix. + +### When to Use Build Artifacts + +- Failed work item has no binlogs (unit tests don't produce them) +- You need to see how tests were **built**, not how they **executed** +- Investigating build/restore issues that happen before Helix + +### Listing Build Artifacts + +```powershell +# List all artifacts for a build +$org = "dnceng-public" +$project = "public" +$buildId = 1280125 + +$url = "https://dev.azure.com/$org/$project/_apis/build/builds/$buildId/artifacts?api-version=5.0" +$artifacts = (Invoke-RestMethod -Uri $url).value + +# Show artifacts with sizes +$artifacts | ForEach-Object { + $sizeMB = [math]::Round($_.resource.properties.artifactsize / 1MB, 2) + Write-Host "$($_.name) - $sizeMB MB" +} +``` + +### Common Build Artifacts + +| Artifact Pattern | Contents | Size | +|------------------|----------|------| +| `TestBuild_*` | Test build outputs + binlogs | 30-100 MB | +| `BuildConfiguration` | Build config metadata | <1 MB | +| `TemplateEngine_*` | Template engine outputs | ~40 MB | +| `AoT_*` | AOT compilation outputs | ~3 MB | +| `FullFramework_*` | .NET Framework test outputs | ~40 MB | + +### Downloading and Finding Binlogs + +```powershell +# Download a specific artifact +$artifactName = "TestBuild_linux_x64" +$downloadUrl = "https://dev.azure.com/$org/$project/_apis/build/builds/$buildId/artifacts?artifactName=$artifactName&api-version=5.0&`$format=zip" +$zipPath = "$env:TEMP\$artifactName.zip" +$extractPath = "$env:TEMP\$artifactName" + +Invoke-WebRequest -Uri $downloadUrl -OutFile $zipPath +Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force + +# Find binlogs +Get-ChildItem -Path $extractPath -Filter "*.binlog" -Recurse | ForEach-Object { + $sizeMB = [math]::Round($_.Length / 1MB, 2) + Write-Host "$($_.Name) ($sizeMB MB) - $($_.FullName)" +} +``` + +### Typical Binlogs in Build Artifacts + +| File | Description | +|------|-------------| +| `log/Release/Build.binlog` | Main build log | +| `log/Release/TestBuildTests.binlog` | Test build verification | +| `log/Release/ToolsetRestore.binlog` | Toolset restore | + +### Build vs Helix Binlogs + +| Source | When Generated | What It Shows | +|--------|----------------|---------------| +| AzDO build artifacts | During CI build phase | How tests were compiled/packaged | +| Helix work item artifacts | During test execution | What happened when tests ran `dotnet build` etc. | + +If a test runs `dotnet build` internally (like SDK end-to-end tests), both sources may have relevant binlogs. + +## Downloaded Artifact Layout + +When you download artifacts via MCP tools or manually, the directory structure can be confusing. Here's what to expect. + +### Helix Work Item Downloads + +MCP tools for downloading Helix artifacts: +- **`hlx_download`** — downloads multiple files from a work item. Returns local file paths. +- **`hlx_download_url`** — downloads a single file by direct URI (from `hlx_files` output). Use when you know exactly which file you need. + +> 💡 **Prefer remote investigation first**: search file contents, parse test results, and search logs remotely before downloading. Only download when you need to load binlogs or do offline analysis. + +`hlx_download` saves files to a temp directory. The structure is **flat** — all files from the work item land in one directory: + +``` +C:\...\Temp\helix-{hash}\ +├── console.d991a56d.log # Console output +├── testResults.xml # Test pass/fail details +├── msbuild.binlog # Only if test invoked MSBuild +├── publish.msbuild.binlog # Only if test did a publish +├── msbuild0.binlog # Numbered: first test's build +├── msbuild1.binlog # Numbered: second test's build +└── core.1000.34 # Only on crash +``` + +**Key confusion point:** Numbered binlogs (`msbuild0.binlog`, `msbuild1.binlog`) correspond to individual test cases within the work item, not to build phases. A work item like `Microsoft.NET.Build.Tests.dll.18` runs dozens of tests, each invoking MSBuild separately. To map a binlog to a specific test: +1. Load it with the binlog analysis tools +2. Check the project paths inside — they usually contain the test name +3. Or check `testResults.xml` to correlate test execution order with binlog numbering + +### AzDO Build Artifact Downloads + +AzDO artifacts download as **ZIP files** with nested directory structures: + +``` +$env:TEMP\TestBuild_linux_x64\ +└── TestBuild_linux_x64\ # Artifact name repeated as subfolder + └── log\Release\ + ├── Build.binlog # Main build + ├── TestBuildTests.binlog # Test build verification + ├── ToolsetRestore.binlog # Toolset restore + └── SendToHelix.binlog # Contains Helix job GUIDs +``` + +**Key confusion point:** The artifact name appears twice in the path (extract folder + subfolder inside the ZIP). Use the full nested path when loading binlogs. + +### Mapping Binlogs to Failures + +This table shows the **typical** source for each binlog type. The boundaries aren't absolute — some repos run tests on the build agent (producing test binlogs in AzDO artifacts), and Helix work items for SDK/Blazor tests invoke `dotnet build` internally (producing build binlogs as Helix artifacts). + +| You want to investigate... | Look here first | But also check... | +|---------------------------|-----------------|-------------------| +| Why a test's internal `dotnet build` failed | Helix work item (`msbuild{N}.binlog`) | AzDO artifact if tests ran on agent | +| Why the CI build itself failed to compile | AzDO build artifact (`Build.binlog`) | — | +| Which Helix jobs were dispatched | AzDO build artifact (`SendToHelix.binlog`) | — | +| AOT compilation failure | Helix work item (`AOTBuild.binlog`) | — | +| Test build/publish behavior | Helix work item (`publish.msbuild.binlog`) | AzDO artifact (`TestBuildTests.binlog`) | + +> **Rule of thumb:** If the failing job name contains "Helix" or "Send to Helix", the test binlogs are in Helix. If the job runs tests directly (common in dotnet/sdk), check AzDO artifacts. + +### Tracking Downloaded Artifacts with SQL + +When downloading from multiple work items (e.g., binlog comparison between passing and failing builds), use SQL to avoid losing track of what's where: + +```sql +CREATE TABLE IF NOT EXISTS downloaded_artifacts ( + local_path TEXT PRIMARY KEY, + helix_job TEXT, + work_item TEXT, + build_id INT, + artifact_source TEXT, -- 'helix' or 'azdo' + file_type TEXT, -- 'binlog', 'testResults', 'console', 'crash' + notes TEXT -- e.g., 'passing baseline', 'failing PR build' +); +``` + +Key queries: +```sql +-- Find the pair of binlogs for comparison +SELECT local_path, notes FROM downloaded_artifacts +WHERE file_type = 'binlog' ORDER BY notes; + +-- What have I downloaded from a specific work item? +SELECT local_path, file_type FROM downloaded_artifacts +WHERE work_item = 'Microsoft.NET.Build.Tests.dll.18'; +``` + +Use this whenever you're juggling artifacts from 2+ Helix jobs (especially during the binlog comparison pattern in [binlog-comparison.md](binlog-comparison.md)). + +### Tips + +- **Multiple binlogs ≠ multiple builds.** A single work item can produce several binlogs if the test suite runs multiple `dotnet build`/`dotnet publish` commands. +- **Helix and AzDO binlogs can overlap.** Helix binlogs are *usually* from test execution and AzDO binlogs from the build phase, but SDK/Blazor tests invoke MSBuild inside Helix (producing build-like binlogs), and some repos run tests directly on the build agent (producing test binlogs in AzDO). Check both sources if you can't find what you need. +- **Not all work items have binlogs.** Standard unit tests only produce `testResults.xml` and console logs. +- **Use `hlx_download` with `pattern:"*.binlog"`** to filter downloads and avoid pulling large console logs. + +## Artifact Retention + +Helix artifacts are retained for a limited time (typically 30 days). Download important artifacts promptly if needed for long-term analysis. diff --git a/.github/skills/ci-analysis/references/manual-investigation.md b/.github/skills/ci-analysis/references/manual-investigation.md new file mode 100644 index 000000000000..b3b319eaacb9 --- /dev/null +++ b/.github/skills/ci-analysis/references/manual-investigation.md @@ -0,0 +1,98 @@ +# Manual Investigation Guide + +If the script doesn't provide enough information, use these manual investigation steps. + +## Table of Contents +- [Get Build Timeline](#get-build-timeline) +- [Find Helix Tasks](#find-helix-tasks) +- [Get Build Logs](#get-build-logs) +- [Query Helix APIs](#query-helix-apis) +- [Download Artifacts](#download-artifacts) +- [Analyze Binlogs](#analyze-binlogs) +- [Extract Environment Variables](#extract-environment-variables) + +## Get Build Timeline + +```powershell +$buildId = 1276327 +$response = Invoke-RestMethod -Uri "https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_apis/build/builds/$buildId/timeline?api-version=7.0" +$failedJobs = $response.records | Where-Object { $_.type -eq "Job" -and $_.result -eq "failed" } +$failedJobs | Select-Object id, name, result | Format-Table +``` + +## Find Helix Tasks + +```powershell +$jobId = "90274d9a-fbd8-54f8-6a7d-8dfc4e2f6f3f" # From timeline +$helixTasks = $response.records | Where-Object { $_.parentId -eq $jobId -and $_.name -like "*Helix*" } +$helixTasks | Select-Object id, name, result, log | Format-Table +``` + +## Get Build Logs + +```powershell +$logId = 565 # From task.log.id +$logContent = Invoke-RestMethod -Uri "https://dev.azure.com/dnceng-public/cbb18261-c48f-4abb-8651-8cdcb5474649/_apis/build/builds/$buildId/logs/${logId}?api-version=7.0" +$logContent | Select-String -Pattern "error|FAIL" -Context 2,5 +``` + +## Query Helix APIs + +> 💡 **Prefer MCP tools when available** — they handle most Helix queries without manual curl commands. Use the APIs below only as fallback. + +```bash +# Get job details +curl -s "https://helix.dot.net/api/2019-06-17/jobs/JOB_ID" + +# List work items +curl -s "https://helix.dot.net/api/2019-06-17/jobs/JOB_ID/workitems" + +# Get work item details +curl -s "https://helix.dot.net/api/2019-06-17/jobs/JOB_ID/workitems/WORK_ITEM_NAME" + +# Get console log +curl -s "https://helix.dot.net/api/2019-06-17/jobs/JOB_ID/workitems/WORK_ITEM_NAME/console" +``` + +## Download Artifacts + +```powershell +$workItem = Invoke-RestMethod -Uri "https://helix.dot.net/api/2019-06-17/jobs/$jobId/workitems/$workItemName" +$workItem.Files | ForEach-Object { Write-Host "$($_.FileName): $($_.Uri)" } +``` + +Common artifacts: +- `console.*.log` - Console output +- `*.binlog` - MSBuild binary logs +- `run-*.log` - XHarness/test runner logs +- Core dumps and crash reports + +## Analyze Binlogs + +Binlogs contain detailed MSBuild execution traces for diagnosing: +- AOT compilation failures +- Static web asset issues +- NuGet restore problems +- Target execution order issues + +**Using MSBuild binlog MCP tools:** + +Load the binlog, then search for errors/diagnostics or specific queries. The binlog MCP tools handle loading, searching, and extracting task details. + +**Manual Analysis:** +Use [MSBuild Structured Log Viewer](https://msbuildlog.com/) or https://live.msbuildlog.com/ + +## Extract Environment Variables + +```bash +curl -s "https://helix.dot.net/api/2019-06-17/jobs/JOB_ID/workitems/WORK_ITEM_NAME/console" | grep "DOTNET_" +``` + +Example output: +``` +DOTNET_JitStress=1 +DOTNET_TieredCompilation=0 +DOTNET_GCStress=0xC +``` + +These are critical for reproducing failures locally. diff --git a/.github/skills/ci-analysis/references/sql-tracking.md b/.github/skills/ci-analysis/references/sql-tracking.md new file mode 100644 index 000000000000..950e2f61a446 --- /dev/null +++ b/.github/skills/ci-analysis/references/sql-tracking.md @@ -0,0 +1,107 @@ +# SQL Tracking for CI Investigations + +Use the SQL tool to track structured data during complex investigations. This avoids losing context across tool calls and enables queries that catch mistakes (like claiming "all failures known" when some are unmatched). + +## Failed Job Tracking + +Track each failure from the script output and map it to known issues as you verify them: + +```sql +CREATE TABLE IF NOT EXISTS failed_jobs ( + build_id INT, + job_name TEXT, + error_category TEXT, -- from failedJobDetails: test-failure, build-error, crash, etc. + error_snippet TEXT, + known_issue_url TEXT, -- NULL if unmatched + known_issue_title TEXT, + is_pr_correlated BOOLEAN DEFAULT FALSE, + recovery_status TEXT DEFAULT 'not-checked', -- effectively-passed, real-failure, no-results + notes TEXT, + PRIMARY KEY (build_id, job_name) +); +``` + +### Key queries + +```sql +-- Unmatched failures (Build Analysis red = these exist) +SELECT job_name, error_category, error_snippet FROM failed_jobs +WHERE known_issue_url IS NULL; + +-- Are ALL failures accounted for? +SELECT COUNT(*) as total, + SUM(CASE WHEN known_issue_url IS NOT NULL THEN 1 ELSE 0 END) as matched +FROM failed_jobs; + +-- Which crash/canceled jobs need recovery verification? +SELECT job_name, build_id FROM failed_jobs +WHERE error_category IN ('crash', 'unclassified') AND recovery_status = 'not-checked'; + +-- PR-correlated failures (fix before retrying) +SELECT job_name, error_snippet FROM failed_jobs WHERE is_pr_correlated = TRUE; +``` + +### Workflow + +1. After the script runs, insert one row per failed job from `failedJobDetails` (each entry includes `buildId`) +2. For each known issue from `knownIssues`, UPDATE matching rows with the issue URL +3. Query for unmatched failures — these need investigation +4. For crash/canceled jobs, update `recovery_status` after checking Helix results + +## Build Progression + +See [build-progression-analysis.md](build-progression-analysis.md) for the `build_progression` and `build_failures` tables that track pass/fail across multiple builds. + +> **`failed_jobs` vs `build_failures` — when to use each:** +> - `failed_jobs` (above): **Job-level** — maps each failed AzDO job to a known issue. Use for single-build triage ("are all failures accounted for?"). +> - `build_failures` (build-progression-analysis.md): **Test-level** — tracks individual test names across builds. Use for progression analysis ("which tests started failing after commit X?"). + +## PR Comment Tracking + +For deep-dive analysis — especially across a chain of related PRs (e.g., dependency flow failures, sequential merge PRs, or long-lived PRs with weeks of triage) — store PR comments so you can query them without re-fetching: + +```sql +CREATE TABLE IF NOT EXISTS pr_comments ( + pr_number INT, + repo TEXT DEFAULT 'dotnet/runtime', + comment_id INT PRIMARY KEY, + author TEXT, + created_at TEXT, + body TEXT, + is_triage BOOLEAN DEFAULT FALSE -- set TRUE if comment diagnoses a failure +); +``` + +### Key queries + +```sql +-- What has already been diagnosed? (avoid re-investigating) +SELECT author, created_at, substr(body, 1, 200) FROM pr_comments +WHERE is_triage = TRUE ORDER BY created_at; + +-- Cross-PR: same failure discussed in multiple PRs? +SELECT pr_number, author, substr(body, 1, 150) FROM pr_comments +WHERE body LIKE '%BlazorWasm%' ORDER BY created_at; + +-- Who was asked to investigate what? +SELECT author, substr(body, 1, 200) FROM pr_comments +WHERE body LIKE '%PTAL%' OR body LIKE '%could you%look%'; +``` + +### When to use + +- Long-lived PRs (>1 week) with 10+ comments containing triage context +- Analyzing a chain of related PRs where earlier PRs have relevant diagnosis +- When the same failure appears across multiple merge/flow PRs and you need to know what was already tried + +## When to Use SQL vs. Not + +| Situation | Use SQL? | +|-----------|----------| +| 1-2 failed jobs, all match known issues | No — straightforward, hold in context | +| 3+ failed jobs across multiple builds | Yes — prevents missed matches | +| Build progression with 5+ builds | Yes — see [build-progression-analysis.md](build-progression-analysis.md) | +| Crash recovery across multiple work items | Yes — cache testResults.xml findings | +| Single build, single failure | No — overkill | +| PR chain or long-lived PR with extensive triage comments | Yes — preserves diagnosis context across tool calls | +| Downloading artifacts from 2+ Helix jobs (e.g., binlog comparison) | Yes — see [helix-artifacts.md](helix-artifacts.md) | diff --git a/.github/skills/ci-analysis/scripts/Get-CIStatus.ps1 b/.github/skills/ci-analysis/scripts/Get-CIStatus.ps1 new file mode 100644 index 000000000000..1d09ae535db3 --- /dev/null +++ b/.github/skills/ci-analysis/scripts/Get-CIStatus.ps1 @@ -0,0 +1,2274 @@ +<# +.SYNOPSIS + Retrieves test failures from Azure DevOps builds and Helix test runs. + +.DESCRIPTION + This script queries Azure DevOps for failed jobs in a build and retrieves + the corresponding Helix console logs to show detailed test failure information. + It can also directly query a specific Helix job and work item. + +.PARAMETER BuildId + The Azure DevOps build ID to query. + +.PARAMETER PRNumber + The GitHub PR number to find the associated build. + +.PARAMETER HelixJob + The Helix job ID (GUID) to query directly. + +.PARAMETER WorkItem + The Helix work item name to query (requires -HelixJob). + +.PARAMETER Repository + The GitHub repository (owner/repo format). Default: dotnet/runtime + +.PARAMETER Organization + The Azure DevOps organization. Default: dnceng-public + +.PARAMETER Project + The Azure DevOps project GUID. Default: cbb18261-c48f-4abb-8651-8cdcb5474649 + +.PARAMETER ShowLogs + If specified, fetches and displays the Helix console logs for failed tests. + +.PARAMETER MaxJobs + Maximum number of failed jobs to process. Default: 5 + +.PARAMETER MaxFailureLines + Maximum number of lines to capture per test failure. Default: 50 + +.PARAMETER TimeoutSec + Timeout in seconds for API calls. Default: 30 + +.PARAMETER ContextLines + Number of context lines to show before errors. Default: 0 + +.PARAMETER NoCache + Bypass cache and fetch fresh data for all API calls. + +.PARAMETER CacheTTLSeconds + Cache lifetime in seconds. Default: 30 + +.PARAMETER ClearCache + Clear all cached files and exit. + +.PARAMETER ContinueOnError + Continue processing remaining jobs if an API call fails, showing partial results. + +.PARAMETER SearchMihuBot + Search MihuBot's semantic database for related issues and discussions. + Uses https://mihubot.xyz/mcp to find conceptually related issues across dotnet repositories. + +.PARAMETER FindBinlogs + Scan work items in a Helix job to find which ones contain MSBuild binlog files. + Useful when the failed work item doesn't have binlogs (e.g., unit tests) but you need + to find related build tests that do have binlogs for deeper analysis. + +.EXAMPLE + .\Get-CIStatus.ps1 -BuildId 1276327 + +.EXAMPLE + .\Get-CIStatus.ps1 -PRNumber 123445 -ShowLogs + +.EXAMPLE + .\Get-CIStatus.ps1 -PRNumber 123445 -Repository dotnet/aspnetcore + +.EXAMPLE + .\Get-CIStatus.ps1 -HelixJob "4b24b2c2-ad5a-4c46-8a84-844be03b1d51" -WorkItem "iOS.Device.Aot.Test" + +.EXAMPLE + .\Get-CIStatus.ps1 -BuildId 1276327 -SearchMihuBot + +.EXAMPLE + .\Get-CIStatus.ps1 -HelixJob "4b24b2c2-ad5a-4c46-8a84-844be03b1d51" -FindBinlogs + # Scans work items to find which ones contain MSBuild binlog files + +.EXAMPLE + .\Get-CIStatus.ps1 -ClearCache +#> + +[CmdletBinding(DefaultParameterSetName = 'BuildId')] +param( + [Parameter(ParameterSetName = 'BuildId', Mandatory = $true)] + [int]$BuildId, + + [Parameter(ParameterSetName = 'PRNumber', Mandatory = $true)] + [int]$PRNumber, + + [Parameter(ParameterSetName = 'HelixJob', Mandatory = $true)] + [string]$HelixJob, + + [Parameter(ParameterSetName = 'HelixJob')] + [string]$WorkItem, + + [Parameter(ParameterSetName = 'ClearCache', Mandatory = $true)] + [switch]$ClearCache, + + [string]$Repository = "dotnet/roslyn", + [string]$Organization = "dnceng-public", + [string]$Project = "cbb18261-c48f-4abb-8651-8cdcb5474649", + [switch]$ShowLogs, + [int]$MaxJobs = 5, + [int]$MaxFailureLines = 50, + [int]$TimeoutSec = 30, + [int]$ContextLines = 0, + [switch]$NoCache, + [int]$CacheTTLSeconds = 30, + [switch]$ContinueOnError, + [switch]$SearchMihuBot, + [switch]$FindBinlogs +) + +$ErrorActionPreference = "Stop" + +#region Caching Functions + +# Cross-platform temp directory detection +function Get-TempDirectory { + # Try common environment variables in order of preference + $tempPath = $env:TEMP + if (-not $tempPath) { $tempPath = $env:TMP } + if (-not $tempPath) { $tempPath = $env:TMPDIR } # macOS + if (-not $tempPath -and $IsLinux) { $tempPath = "/tmp" } + if (-not $tempPath -and $IsMacOS) { $tempPath = "/tmp" } + if (-not $tempPath) { + # Fallback: use .cache in user's home directory + $home = $env:HOME + if (-not $home) { $home = $env:USERPROFILE } + if ($home) { + $tempPath = Join-Path $home ".cache" + if (-not (Test-Path $tempPath)) { + New-Item -ItemType Directory -Path $tempPath -Force | Out-Null + } + } + } + if (-not $tempPath) { + throw "Could not determine temp directory. Set TEMP, TMP, or TMPDIR environment variable." + } + return $tempPath +} + +$script:TempDir = Get-TempDirectory + +# Handle -ClearCache parameter +if ($ClearCache) { + $cacheDir = Join-Path $script:TempDir "ci-analysis-cache" + if (Test-Path $cacheDir) { + $files = Get-ChildItem -Path $cacheDir -File + $count = $files.Count + Remove-Item -Path $cacheDir -Recurse -Force + Write-Host "Cleared $count cached files from $cacheDir" -ForegroundColor Green + } + else { + Write-Host "Cache directory does not exist: $cacheDir" -ForegroundColor Yellow + } + exit 0 +} + +# Setup caching +$script:CacheDir = Join-Path $script:TempDir "ci-analysis-cache" +if (-not (Test-Path $script:CacheDir)) { + New-Item -ItemType Directory -Path $script:CacheDir -Force | Out-Null +} + +# Clean up expired cache files on startup (files older than 2x TTL) +function Clear-ExpiredCache { + param([int]$TTLSeconds = $CacheTTLSeconds) + + $maxAge = $TTLSeconds * 2 + $cutoff = (Get-Date).AddSeconds(-$maxAge) + + Get-ChildItem -Path $script:CacheDir -File -ErrorAction SilentlyContinue | Where-Object { + $_.LastWriteTime -lt $cutoff + } | ForEach-Object { + Write-Verbose "Removing expired cache file: $($_.Name)" + try { + Remove-Item $_.FullName -Force -ErrorAction Stop + } + catch { + Write-Verbose "Failed to remove cache file '$($_.Name)': $($_.Exception.Message)" + } + } +} + +# Run cache cleanup at startup (non-blocking) +if (-not $NoCache) { + Clear-ExpiredCache -TTLSeconds $CacheTTLSeconds +} + +function Get-UrlHash { + param([string]$Url) + + $sha256 = [System.Security.Cryptography.SHA256]::Create() + try { + return [System.BitConverter]::ToString( + $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($Url)) + ).Replace("-", "") + } + finally { + $sha256.Dispose() + } +} + +function Get-CachedResponse { + param( + [string]$Url, + [int]$TTLSeconds = $CacheTTLSeconds + ) + + if ($NoCache) { return $null } + + $hash = Get-UrlHash -Url $Url + $cacheFile = Join-Path $script:CacheDir "$hash.json" + + if (Test-Path $cacheFile) { + $cacheInfo = Get-Item $cacheFile + $age = (Get-Date) - $cacheInfo.LastWriteTime + + if ($age.TotalSeconds -lt $TTLSeconds) { + Write-Verbose "Cache hit for $Url (age: $([int]$age.TotalSeconds) sec)" + return Get-Content $cacheFile -Raw + } + else { + Write-Verbose "Cache expired for $Url" + } + } + + return $null +} + +function Set-CachedResponse { + param( + [string]$Url, + [string]$Content + ) + + if ($NoCache) { return } + + $hash = Get-UrlHash -Url $Url + $cacheFile = Join-Path $script:CacheDir "$hash.json" + + # Use atomic write: write to temp file, then rename + $tempFile = Join-Path $script:CacheDir "$hash.tmp.$([System.Guid]::NewGuid().ToString('N'))" + try { + $Content | Set-Content -LiteralPath $tempFile -Force + Move-Item -LiteralPath $tempFile -Destination $cacheFile -Force + Write-Verbose "Cached response for $Url" + } + catch { + # Clean up temp file on failure + if (Test-Path $tempFile) { + Remove-Item -LiteralPath $tempFile -Force -ErrorAction SilentlyContinue + } + Write-Verbose "Failed to cache response: $_" + } +} + +function Invoke-CachedRestMethod { + param( + [string]$Uri, + [int]$TimeoutSec = 30, + [switch]$AsJson, + [switch]$SkipCache, + [switch]$SkipCacheWrite + ) + + # Check cache first (unless skipping) + if (-not $SkipCache) { + $cached = Get-CachedResponse -Url $Uri + if ($cached) { + if ($AsJson) { + try { + return $cached | ConvertFrom-Json -ErrorAction Stop + } + catch { + Write-Verbose "Failed to parse cached response as JSON, treating as cache miss: $_" + } + } + else { + return $cached + } + } + } + + # Make the actual request + Write-Verbose "GET $Uri" + $response = Invoke-RestMethod -Uri $Uri -Method Get -TimeoutSec $TimeoutSec + + # Cache the response (unless skipping write) + if (-not $SkipCache -and -not $SkipCacheWrite) { + if ($AsJson -or $response -is [PSCustomObject]) { + $content = $response | ConvertTo-Json -Depth 100 -Compress + Set-CachedResponse -Url $Uri -Content $content + } + else { + Set-CachedResponse -Url $Uri -Content $response + } + } + + return $response +} + +#endregion Caching Functions + +#region Validation Functions + +function Test-RepositoryFormat { + param([string]$Repo) + + # Validate repository format to prevent command injection + $repoPattern = '^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$' + if ($Repo -notmatch $repoPattern) { + throw "Invalid repository format '$Repo'. Expected 'owner/repo' (e.g., 'dotnet/runtime')." + } + return $true +} + +function Get-SafeSearchTerm { + param([string]$Term) + + # Sanitize search term to avoid passing unsafe characters to gh CLI + # Keep: alphanumeric, spaces, dots, hyphens, colons (for namespaces like System.Net), + # and slashes (for paths). These are safe for GitHub search and common in .NET names. + $safeTerm = $Term -replace '[^\w\s\-.:/]', '' + return $safeTerm.Trim() +} + +#endregion Validation Functions + +#region Azure DevOps API Functions + +function Get-AzDOBuildIdFromPR { + param([int]$PR) + + # Check for gh CLI dependency + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + throw "GitHub CLI (gh) is required for PR lookup. Install from https://cli.github.com/ or use -BuildId instead." + } + + # Validate repository format + Test-RepositoryFormat -Repo $Repository | Out-Null + + Write-Host "Finding builds for PR #$PR in $Repository..." -ForegroundColor Cyan + Write-Verbose "Running: gh pr checks $PR --repo $Repository" + + # Use gh cli to get the checks with splatted arguments + $checksOutput = & gh pr checks $PR --repo $Repository 2>&1 + $ghExitCode = $LASTEXITCODE + + if ($ghExitCode -ne 0 -and -not ($checksOutput | Select-String -Pattern "buildId=")) { + throw "Failed to fetch CI status for PR #$PR in $Repository - check PR number and permissions" + } + + # Check if PR has merge conflicts (no CI runs when mergeable_state is dirty) + $prMergeState = $null + $prMergeStateOutput = & gh api "repos/$Repository/pulls/$PR" --jq '.mergeable_state' 2>$null + $ghMergeStateExitCode = $LASTEXITCODE + if ($ghMergeStateExitCode -eq 0 -and $prMergeStateOutput) { + $prMergeState = $prMergeStateOutput.Trim() + } else { + Write-Verbose "Could not determine PR merge state (gh exit code $ghMergeStateExitCode)." + } + + # Find ALL failing Azure DevOps builds + $failingBuilds = @{} + foreach ($line in $checksOutput) { + if ($line -match 'fail.*buildId=(\d+)') { + $buildId = $Matches[1] + # Extract pipeline name (first column before 'fail') + $pipelineName = ($line -split '\s+fail')[0].Trim() + if (-not $failingBuilds.ContainsKey($buildId)) { + $failingBuilds[$buildId] = $pipelineName + } + } + } + + if ($failingBuilds.Count -eq 0) { + # No failing builds - try to find any build + $anyBuild = $checksOutput | Select-String -Pattern "buildId=(\d+)" | Select-Object -First 1 + if ($anyBuild) { + $anyBuildMatch = [regex]::Match($anyBuild.ToString(), "buildId=(\d+)") + if ($anyBuildMatch.Success) { + $buildIdStr = $anyBuildMatch.Groups[1].Value + $buildIdInt = 0 + if ([int]::TryParse($buildIdStr, [ref]$buildIdInt)) { + return @{ BuildIds = @($buildIdInt); Reason = $null; MergeState = $prMergeState } + } + } + } + if ($prMergeState -eq 'dirty') { + Write-Host "`nPR #$PR has merge conflicts (mergeable_state: dirty)" -ForegroundColor Red + Write-Host "CI will not run until conflicts are resolved." -ForegroundColor Yellow + Write-Host "Resolve conflicts and push to trigger CI, or use -BuildId to analyze a previous build." -ForegroundColor Gray + return @{ BuildIds = @(); Reason = "MERGE_CONFLICTS"; MergeState = $prMergeState } + } + Write-Host "`nNo CI build found for PR #$PR in $Repository" -ForegroundColor Red + Write-Host "The CI pipeline has not been triggered yet." -ForegroundColor Yellow + return @{ BuildIds = @(); Reason = "NO_BUILDS"; MergeState = $prMergeState } + } + + # Return all unique failing build IDs + $buildIds = $failingBuilds.Keys | ForEach-Object { [int]$_ } | Sort-Object -Unique + + if ($buildIds.Count -gt 1) { + Write-Host "Found $($buildIds.Count) failing builds:" -ForegroundColor Yellow + foreach ($id in $buildIds) { + Write-Host " - Build $id ($($failingBuilds[$id.ToString()]))" -ForegroundColor Gray + } + } + + return @{ BuildIds = $buildIds; Reason = $null; MergeState = $prMergeState } +} + +function Get-BuildAnalysisKnownIssues { + param([int]$PR) + + # Check for gh CLI dependency + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Verbose "GitHub CLI (gh) not available for Build Analysis check" + return @() + } + + Write-Verbose "Fetching Build Analysis check for PR #$PR..." + + try { + # Get the head commit SHA for the PR + $headSha = gh pr view $PR --repo $Repository --json headRefOid --jq '.headRefOid' 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Verbose "Failed to get PR head SHA: $headSha" + return @() + } + + # Validate headSha is a valid git SHA (40 hex characters) + if ($headSha -notmatch '^[a-fA-F0-9]{40}$') { + Write-Verbose "Invalid head SHA format: $headSha" + return @() + } + + # Get the Build Analysis check run + $checkRuns = gh api "repos/$Repository/commits/$headSha/check-runs" --jq '.check_runs[] | select(.name == "Build Analysis") | .output' 2>&1 + if ($LASTEXITCODE -ne 0 -or -not $checkRuns) { + Write-Verbose "No Build Analysis check found" + return @() + } + + $output = $checkRuns | ConvertFrom-Json -ErrorAction SilentlyContinue + if (-not $output -or -not $output.text) { + Write-Verbose "Build Analysis check has no output text" + return @() + } + + # Parse known issues from the output text + # Format: Issue Title + $knownIssues = @() + $issuePattern = '([^<]+)' + $matches = [regex]::Matches($output.text, $issuePattern) + + foreach ($match in $matches) { + $issueUrl = $match.Groups[1].Value + $issueNumber = $match.Groups[2].Value + $issueTitle = $match.Groups[3].Value + + # Avoid duplicates + if (-not ($knownIssues | Where-Object { $_.Number -eq $issueNumber })) { + $knownIssues += @{ + Number = $issueNumber + Url = $issueUrl + Title = $issueTitle + } + } + } + + if ($knownIssues.Count -gt 0) { + Write-Host "`nBuild Analysis found $($knownIssues.Count) known issue(s):" -ForegroundColor Yellow + foreach ($issue in $knownIssues) { + Write-Host " - #$($issue.Number): $($issue.Title)" -ForegroundColor Gray + Write-Host " $($issue.Url)" -ForegroundColor DarkGray + } + } + + return $knownIssues + } + catch { + Write-Verbose "Error fetching Build Analysis: $_" + return @() + } +} + +function Get-PRChangedFiles { + param( + [int]$PR, + [int]$MaxFiles = 100 + ) + + # Check for gh CLI dependency + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Verbose "GitHub CLI (gh) not available for PR file lookup" + return @() + } + + Write-Verbose "Fetching changed files for PR #$PR..." + + try { + # Get the file count first to avoid fetching huge PRs + $fileCount = gh pr view $PR --repo $Repository --json files --jq '.files | length' 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Verbose "Failed to get PR file count: $fileCount" + return @() + } + + $count = [int]$fileCount + if ($count -gt $MaxFiles) { + Write-Verbose "PR has $count files (exceeds limit of $MaxFiles) - skipping correlation" + Write-Host "PR has $count changed files - skipping detailed correlation (limit: $MaxFiles)" -ForegroundColor Gray + return @() + } + + # Get the list of changed files + $filesJson = gh pr view $PR --repo $Repository --json files --jq '.files[].path' 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Verbose "Failed to get PR files: $filesJson" + return @() + } + + $files = $filesJson -split "`n" | Where-Object { $_ } + return $files + } + catch { + Write-Verbose "Error fetching PR files: $_" + return @() + } +} + +function Get-PRCorrelation { + param( + [array]$ChangedFiles, + [array]$AllFailures + ) + + $result = @{ CorrelatedFiles = @(); TestFiles = @() } + if ($ChangedFiles.Count -eq 0 -or $AllFailures.Count -eq 0) { return $result } + + $failureText = ($AllFailures | ForEach-Object { + $_.TaskName + $_.JobName + $_.Errors -join "`n" + $_.HelixLogs -join "`n" + $_.FailedTests -join "`n" + }) -join "`n" + + foreach ($file in $ChangedFiles) { + $fileName = [System.IO.Path]::GetFileNameWithoutExtension($file) + $fileNameWithExt = [System.IO.Path]::GetFileName($file) + $baseTestName = $fileName -replace '\.[^.]+$', '' + + $isCorrelated = $false + if ($failureText -match [regex]::Escape($fileName) -or + $failureText -match [regex]::Escape($fileNameWithExt) -or + $failureText -match [regex]::Escape($file) -or + ($baseTestName -and $failureText -match [regex]::Escape($baseTestName))) { + $isCorrelated = $true + } + + if ($isCorrelated) { + $isTestFile = $file -match '\.Tests?\.' -or $file -match '[/\\]tests?[/\\]' -or $file -match 'Test\.cs$' -or $file -match 'Tests\.cs$' + if ($isTestFile) { $result.TestFiles += $file } else { $result.CorrelatedFiles += $file } + } + } + + $result.CorrelatedFiles = @($result.CorrelatedFiles | Select-Object -Unique) + $result.TestFiles = @($result.TestFiles | Select-Object -Unique) + return $result +} + +function Show-PRCorrelationSummary { + param( + [array]$ChangedFiles, + [array]$AllFailures + ) + + if ($ChangedFiles.Count -eq 0) { + return + } + + $correlation = Get-PRCorrelation -ChangedFiles $ChangedFiles -AllFailures $AllFailures + $correlatedFiles = $correlation.CorrelatedFiles + $testFiles = $correlation.TestFiles + + # Show results + if ($correlatedFiles.Count -gt 0 -or $testFiles.Count -gt 0) { + Write-Host "`n=== PR Change Correlation ===" -ForegroundColor Magenta + + if ($testFiles.Count -gt 0) { + Write-Host "⚠️ Test files changed by this PR are failing:" -ForegroundColor Yellow + $shown = 0 + foreach ($file in $testFiles) { + if ($shown -ge 10) { + Write-Host " ... and $($testFiles.Count - 10) more test files" -ForegroundColor Gray + break + } + Write-Host " $file" -ForegroundColor Red + $shown++ + } + } + + if ($correlatedFiles.Count -gt 0) { + Write-Host "⚠️ Files changed by this PR appear in failures:" -ForegroundColor Yellow + $shown = 0 + foreach ($file in $correlatedFiles) { + if ($shown -ge 10) { + Write-Host " ... and $($correlatedFiles.Count - 10) more files" -ForegroundColor Gray + break + } + Write-Host " $file" -ForegroundColor Red + $shown++ + } + } + + Write-Host "`nCorrelated files found — check JSON summary for details." -ForegroundColor Yellow + } +} + +function Get-AzDOBuildStatus { + param([int]$Build) + + $url = "https://dev.azure.com/$Organization/$Project/_apis/build/builds/${Build}?api-version=7.0" + + try { + # First check cache to see if we have a completed status + $cached = Get-CachedResponse -Url $url + if ($cached) { + $cachedData = $cached | ConvertFrom-Json + # Only use cache if build was completed - in-progress status goes stale quickly + if ($cachedData.status -eq "completed") { + return @{ + Status = $cachedData.status + Result = $cachedData.result + StartTime = $cachedData.startTime + FinishTime = $cachedData.finishTime + } + } + Write-Verbose "Skipping cached in-progress build status" + } + + # Fetch fresh status + $response = Invoke-CachedRestMethod -Uri $url -TimeoutSec $TimeoutSec -AsJson -SkipCache + + # Only cache if completed + if ($response.status -eq "completed") { + $content = $response | ConvertTo-Json -Depth 10 -Compress + Set-CachedResponse -Url $url -Content $content + } + + return @{ + Status = $response.status # notStarted, inProgress, completed + Result = $response.result # succeeded, failed, canceled (only set when completed) + StartTime = $response.startTime + FinishTime = $response.finishTime + } + } + catch { + Write-Verbose "Failed to fetch build status: $_" + return $null + } +} + +function Get-AzDOTimeline { + param( + [int]$Build, + [switch]$BuildInProgress + ) + + $url = "https://dev.azure.com/$Organization/$Project/_apis/build/builds/$Build/timeline?api-version=7.0" + Write-Host "Fetching build timeline..." -ForegroundColor Cyan + + try { + # Don't cache timeline for in-progress builds - it changes as jobs complete + $response = Invoke-CachedRestMethod -Uri $url -TimeoutSec $TimeoutSec -AsJson -SkipCacheWrite:$BuildInProgress + return $response + } + catch { + if ($ContinueOnError) { + Write-Warning "Failed to fetch build timeline: $_" + return $null + } + throw "Failed to fetch build timeline: $_" + } +} + +function Get-FailedJobs { + param($Timeline) + + if ($null -eq $Timeline -or $null -eq $Timeline.records) { + return @() + } + + $failedJobs = $Timeline.records | Where-Object { + $_.type -eq "Job" -and $_.result -eq "failed" + } + + return $failedJobs +} + +function Get-CanceledJobs { + param($Timeline) + + if ($null -eq $Timeline -or $null -eq $Timeline.records) { + return @() + } + + $canceledJobs = $Timeline.records | Where-Object { + $_.type -eq "Job" -and $_.result -eq "canceled" + } + + return $canceledJobs +} + +function Get-HelixJobInfo { + param($Timeline, $JobId) + + if ($null -eq $Timeline -or $null -eq $Timeline.records) { + return @() + } + + # Find tasks in this job that mention Helix + $helixTasks = $Timeline.records | Where-Object { + $_.parentId -eq $JobId -and + $_.name -like "*Helix*" -and + $_.result -eq "failed" + } + + return $helixTasks +} + +function Get-BuildLog { + param([int]$Build, [int]$LogId) + + $url = "https://dev.azure.com/$Organization/$Project/_apis/build/builds/$Build/logs/${LogId}?api-version=7.0" + + try { + $response = Invoke-CachedRestMethod -Uri $url -TimeoutSec $TimeoutSec + return $response + } + catch { + Write-Warning "Failed to fetch log ${LogId}: $_" + return $null + } +} + +#endregion Azure DevOps API Functions + +#region Log Parsing Functions + +function Extract-HelixUrls { + param([string]$LogContent) + + $urls = @() + + # First, normalize the content by removing line breaks that might split URLs + $normalizedContent = $LogContent -replace "`r`n", "" -replace "`n", "" + + # Match Helix console log URLs - workitem names can contain dots, dashes, and other chars + $urlMatches = [regex]::Matches($normalizedContent, 'https://helix\.dot\.net/api/[^/]+/jobs/[a-f0-9-]+/workitems/[^/\s]+/console') + foreach ($match in $urlMatches) { + $urls += $match.Value + } + + Write-Verbose "Found $($urls.Count) Helix URLs" + return $urls | Select-Object -Unique +} + +function Extract-TestFailures { + param([string]$LogContent) + + $failures = @() + + # Match test failure patterns from MSBuild output + $pattern = 'error\s*:\s*.*Test\s+(\S+)\s+has failed' + $failureMatches = [regex]::Matches($LogContent, $pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + + foreach ($match in $failureMatches) { + $failures += @{ + TestName = $match.Groups[1].Value + FullMatch = $match.Value + } + } + + Write-Verbose "Found $($failures.Count) test failures" + return $failures +} + +function Extract-BuildErrors { + param( + [string]$LogContent, + [int]$Context = 5 + ) + + $errors = @() + $lines = $LogContent -split "`n" + + # Patterns for common build errors - ordered from most specific to least specific + $errorPatterns = @( + 'error\s+CS\d+:.*', # C# compiler errors + 'error\s+MSB\d+:.*', # MSBuild errors + 'error\s+NU\d+:.*', # NuGet errors + '\.pcm: No such file or directory', # Clang module cache + 'EXEC\s*:\s*error\s*:.*', # Exec task errors + 'fatal error:.*', # Fatal errors (clang, etc) + ':\s*error:', # Clang/GCC errors (file.cpp:123: error:) + 'undefined reference to', # Linker errors + 'cannot find -l', # Linker missing library + 'collect2: error:', # GCC linker wrapper errors + '##\[error\].*' # AzDO error annotations (last - catch-all) + ) + + $combinedPattern = ($errorPatterns -join '|') + + # Track if we only found MSBuild wrapper errors + $foundRealErrors = $false + $msbWrapperLines = @() + + for ($i = 0; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match $combinedPattern) { + # Skip MSBuild wrapper "exited with code" if we find real errors + if ($lines[$i] -match 'exited with code \d+') { + $msbWrapperLines += $i + continue + } + + # Skip duplicate MSBuild errors (they often repeat) + if ($lines[$i] -match 'error MSB3073.*exited with code') { + continue + } + + $foundRealErrors = $true + + # Clean up the line (remove timestamps, etc) + $cleanLine = $lines[$i] -replace '^\d{4}-\d{2}-\d{2}T[\d:\.]+Z\s*', '' + $cleanLine = $cleanLine -replace '##\[error\]', 'ERROR: ' + + # Add context lines if requested + if ($Context -gt 0) { + $contextStart = [Math]::Max(0, $i - $Context) + $contextLines = @() + for ($j = $contextStart; $j -lt $i; $j++) { + $contextLines += " " + $lines[$j].Trim() + } + if ($contextLines.Count -gt 0) { + $errors += ($contextLines -join "`n") + } + } + + $errors += $cleanLine.Trim() + } + } + + # If we only found MSBuild wrapper errors, show context around them + if (-not $foundRealErrors -and $msbWrapperLines.Count -gt 0) { + $wrapperLine = $msbWrapperLines[0] + # Look for real errors in the 50 lines before the wrapper error + $searchStart = [Math]::Max(0, $wrapperLine - 50) + for ($i = $searchStart; $i -lt $wrapperLine; $i++) { + $line = $lines[$i] + # Look for C++/clang/gcc style errors + if ($line -match ':\s*error:' -or $line -match 'fatal error:' -or $line -match 'undefined reference') { + $cleanLine = $line -replace '^\d{4}-\d{2}-\d{2}T[\d:\.]+Z\s*', '' + $errors += $cleanLine.Trim() + } + } + } + + return $errors | Select-Object -First 20 | Select-Object -Unique +} + +function Extract-HelixLogUrls { + param([string]$LogContent) + + $urls = @() + + # Match Helix console log URLs from log content + # Pattern: https://helix.dot.net/api/2019-06-17/jobs/{jobId}/workitems/{workItemName}/console + $pattern = 'https://helix\.dot\.net/api/[^/]+/jobs/([a-f0-9-]+)/workitems/([^/\s]+)/console' + $urlMatches = [regex]::Matches($LogContent, $pattern) + + foreach ($match in $urlMatches) { + $urls += @{ + Url = $match.Value + JobId = $match.Groups[1].Value + WorkItem = $match.Groups[2].Value + } + } + + # Deduplicate by URL + $uniqueUrls = @{} + foreach ($url in $urls) { + if (-not $uniqueUrls.ContainsKey($url.Url)) { + $uniqueUrls[$url.Url] = $url + } + } + + return $uniqueUrls.Values +} + +#endregion Log Parsing Functions + +#region Known Issues Search + +function Search-MihuBotIssues { + param( + [string[]]$SearchTerms, + [string]$ExtraContext = "", + [string]$Repository = "dotnet/runtime", + [bool]$IncludeOpen = $true, + [bool]$IncludeClosed = $true, + [int]$TimeoutSec = 30 + ) + + $results = @() + + if (-not $SearchTerms -or $SearchTerms.Count -eq 0) { + return $results + } + + try { + # MihuBot MCP endpoint - call as JSON-RPC style request + $mcpUrl = "https://mihubot.xyz/mcp" + + # Build the request payload matching the MCP tool schema + $payload = @{ + jsonrpc = "2.0" + method = "tools/call" + id = [guid]::NewGuid().ToString() + params = @{ + name = "search_dotnet_repos" + arguments = @{ + repository = $Repository + searchTerms = $SearchTerms + extraSearchContext = $ExtraContext + includeOpen = $IncludeOpen + includeClosed = $IncludeClosed + includeIssues = $true + includePullRequests = $true + includeComments = $false + } + } + } | ConvertTo-Json -Depth 10 + + Write-Verbose "Calling MihuBot MCP endpoint with terms: $($SearchTerms -join ', ')" + + $response = Invoke-RestMethod -Uri $mcpUrl -Method Post -Body $payload -ContentType "application/json" -TimeoutSec $TimeoutSec + + # Parse MCP response + if ($response.result -and $response.result.content) { + foreach ($content in $response.result.content) { + if ($content.type -eq "text" -and $content.text) { + $issueData = $content.text | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($issueData) { + foreach ($issue in $issueData) { + $results += @{ + Number = $issue.Number + Title = $issue.Title + Url = $issue.Url + Repository = $issue.Repository + State = $issue.State + Source = "MihuBot" + } + } + } + } + } + } + + # Deduplicate by issue number and repo + $unique = @{} + foreach ($issue in $results) { + $key = "$($issue.Repository)#$($issue.Number)" + if (-not $unique.ContainsKey($key)) { + $unique[$key] = $issue + } + } + + return $unique.Values | Select-Object -First 5 + } + catch { + Write-Verbose "MihuBot search failed: $_" + return @() + } +} + +function Search-KnownIssues { + param( + [string]$TestName, + [string]$ErrorMessage, + [string]$Repository = "dotnet/runtime" + ) + + # Search for known issues using the "Known Build Error" label + # This label is used by Build Analysis across dotnet repositories + + $knownIssues = @() + + # Check if gh CLI is available + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Verbose "GitHub CLI not available for searching known issues" + return $knownIssues + } + + try { + # Extract search terms from test name and error message + $searchTerms = @() + + # First priority: Look for [FAIL] test names in the error message + # Pattern: "TestName [FAIL]" - the test name comes BEFORE [FAIL] + if ($ErrorMessage -match '(\S+)\s+\[FAIL\]') { + $failedTest = $Matches[1] + # Extract just the method name (after last .) + if ($failedTest -match '\.([^.]+)$') { + $searchTerms += $Matches[1] + } + # Also add the full test name + $searchTerms += $failedTest + } + + # Second priority: Extract test class/method from stack traces + if ($ErrorMessage -match 'at\s+(\w+\.\w+)\(' -and $searchTerms.Count -eq 0) { + $searchTerms += $Matches[1] + } + + if ($TestName) { + # Try to get the test method name from the work item + if ($TestName -match '\.([^.]+)$') { + $methodName = $Matches[1] + # Only add if it looks like a test name (not just "Tests") + if ($methodName -ne "Tests" -and $methodName.Length -gt 5) { + $searchTerms += $methodName + } + } + # Also try the full test name if it's not too long and looks specific + if ($TestName.Length -lt 100 -and $TestName -notmatch '^System\.\w+\.Tests$') { + $searchTerms += $TestName + } + } + + # Third priority: Extract specific exception patterns (but not generic TimeoutException) + if ($ErrorMessage -and $searchTerms.Count -eq 0) { + # Look for specific exception types + if ($ErrorMessage -match '(System\.(?:InvalidOperation|ArgumentNull|Format)\w*Exception)') { + $searchTerms += $Matches[1] + } + } + + # Deduplicate and limit search terms + $searchTerms = $searchTerms | Select-Object -Unique | Select-Object -First 3 + + foreach ($term in $searchTerms) { + if (-not $term) { continue } + + # Sanitize the search term to avoid passing unsafe characters to gh CLI + $safeTerm = Get-SafeSearchTerm -Term $term + if (-not $safeTerm) { continue } + + Write-Verbose "Searching for known issues with term: $safeTerm" + + # Search for open issues with the "Known Build Error" label + $results = & gh issue list ` + --repo $Repository ` + --label "Known Build Error" ` + --state open ` + --search $safeTerm ` + --limit 3 ` + --json number,title,url 2>$null | ConvertFrom-Json + + if ($results) { + foreach ($issue in $results) { + # Check if the title actually contains our search term (avoid false positives) + if ($issue.title -match [regex]::Escape($safeTerm)) { + $knownIssues += @{ + Number = $issue.number + Title = $issue.title + Url = $issue.url + SearchTerm = $safeTerm + } + } + } + } + + # If we found issues, stop searching + if ($knownIssues.Count -gt 0) { + break + } + } + + # Deduplicate by issue number + $unique = @{} + foreach ($issue in $knownIssues) { + if (-not $unique.ContainsKey($issue.Number)) { + $unique[$issue.Number] = $issue + } + } + + return $unique.Values + } + catch { + Write-Verbose "Failed to search for known issues: $_" + return @() + } +} + +function Show-KnownIssues { + param( + [string]$TestName = "", + [string]$ErrorMessage = "", + [string]$Repository = $script:Repository, + [switch]$IncludeMihuBot + ) + + # Search for known issues if we have a test name or error + if ($TestName -or $ErrorMessage) { + $knownIssues = Search-KnownIssues -TestName $TestName -ErrorMessage $ErrorMessage -Repository $Repository + if ($knownIssues -and $knownIssues.Count -gt 0) { + Write-Host "`n Known Issues:" -ForegroundColor Magenta + foreach ($issue in $knownIssues) { + Write-Host " #$($issue.Number): $($issue.Title)" -ForegroundColor Magenta + Write-Host " $($issue.Url)" -ForegroundColor Gray + } + } + + # Search MihuBot for related issues/discussions + if ($IncludeMihuBot) { + $searchTerms = @() + + # Extract meaningful search terms + if ($ErrorMessage -match '(\S+)\s+\[FAIL\]') { + $failedTest = $Matches[1] + if ($failedTest -match '\.([^.]+)$') { + $searchTerms += $Matches[1] + } + } + + if ($TestName -and $TestName -match '\.([^.]+)$') { + $methodName = $Matches[1] + if ($methodName -ne "Tests" -and $methodName.Length -gt 5) { + $searchTerms += $methodName + } + } + + # Add test name as context + if ($TestName) { + $searchTerms += $TestName + } + + $searchTerms = $searchTerms | Select-Object -Unique | Select-Object -First 3 + + if ($searchTerms.Count -gt 0) { + $mihuBotResults = Search-MihuBotIssues -SearchTerms $searchTerms -Repository $Repository -ExtraContext "test failure $TestName" + if ($mihuBotResults -and $mihuBotResults.Count -gt 0) { + # Filter out issues already shown from Known Build Error search + $knownNumbers = @() + if ($knownIssues) { + $knownNumbers = $knownIssues | ForEach-Object { $_.Number } + } + $newResults = $mihuBotResults | Where-Object { $_.Number -notin $knownNumbers } + + if ($newResults -and @($newResults).Count -gt 0) { + Write-Host "`n Related Issues (MihuBot):" -ForegroundColor Blue + foreach ($issue in $newResults) { + $stateIcon = if ($issue.State -eq "open") { "[open]" } else { "[closed]" } + Write-Host " #$($issue.Number): $($issue.Title) $stateIcon" -ForegroundColor Blue + Write-Host " $($issue.Url)" -ForegroundColor Gray + } + } + } + } + } + } +} + +#endregion Known Issues Search + +#region Test Results Functions + +function Get-AzDOTestResults { + param( + [string]$RunId, + [string]$Org = "https://dev.azure.com/$Organization" + ) + + # Check if az devops CLI is available + if (-not (Get-Command az -ErrorAction SilentlyContinue)) { + Write-Verbose "Azure CLI not available for fetching test results" + return $null + } + + try { + Write-Verbose "Fetching test results for run $RunId via az devops CLI..." + $results = az devops invoke ` + --org $Org ` + --area test ` + --resource Results ` + --route-parameters project=$Project runId=$RunId ` + --api-version 7.0 ` + --query "value[?outcome=='Failed'].{name:testCaseTitle, outcome:outcome, error:errorMessage}" ` + -o json 2>$null | ConvertFrom-Json + + return $results + } + catch { + Write-Verbose "Failed to fetch test results via az devops: $_" + return $null + } +} + +function Extract-TestRunUrls { + param([string]$LogContent) + + $testRuns = @() + + # Match Azure DevOps Test Run URLs + # Pattern: Published Test Run : https://dev.azure.com/dnceng-public/public/_TestManagement/Runs?runId=35626550&_a=runCharts + $pattern = 'Published Test Run\s*:\s*(https://dev\.azure\.com/[^/]+/[^/]+/_TestManagement/Runs\?runId=(\d+)[^\s]*)' + $matches = [regex]::Matches($LogContent, $pattern) + + foreach ($match in $matches) { + $testRuns += @{ + Url = $match.Groups[1].Value + RunId = $match.Groups[2].Value + } + } + + Write-Verbose "Found $($testRuns.Count) test run URLs" + return $testRuns +} + +function Get-LocalTestFailures { + param( + [object]$Timeline, + [int]$BuildId + ) + + $localFailures = @() + + # Find failed test tasks (non-Helix) + # Look for tasks with "Test" in name that have issues but no Helix URLs + $testTasks = $Timeline.records | Where-Object { + ($_.name -match 'Test|xUnit' -or $_.type -eq 'Task') -and + $_.issues -and + $_.issues.Count -gt 0 + } + + foreach ($task in $testTasks) { + # Check if this task has test failures (XUnit errors) + $testErrors = $task.issues | Where-Object { + $_.message -match 'Tests failed:' -or + $_.message -match 'error\s*:.*Test.*failed' + } + + if ($testErrors.Count -gt 0) { + # This is a local test failure - find the parent job for URL construction + $parentJob = $Timeline.records | Where-Object { $_.id -eq $task.parentId -and $_.type -eq "Job" } | Select-Object -First 1 + + $failure = @{ + TaskName = $task.name + TaskId = $task.id + ParentJobId = if ($parentJob) { $parentJob.id } else { $task.parentId } + LogId = if ($task.log) { $task.log.id } else { $null } + Issues = $testErrors + TestRunUrls = @() + } + + # Try to get test run URLs from the publish task + $publishTask = $Timeline.records | Where-Object { + $_.parentId -eq $task.parentId -and + $_.name -match 'Publish.*Test.*Results' -and + $_.log + } | Select-Object -First 1 + + if ($publishTask -and $publishTask.log) { + $logContent = Get-BuildLog -Build $BuildId -LogId $publishTask.log.id + if ($logContent) { + $testRunUrls = Extract-TestRunUrls -LogContent $logContent + $failure.TestRunUrls = $testRunUrls + } + } + + $localFailures += $failure + } + } + + return $localFailures +} + +#endregion Test Results Functions + +#region Helix API Functions + +function Get-HelixJobDetails { + param([string]$JobId) + + $url = "https://helix.dot.net/api/2019-06-17/jobs/$JobId" + + try { + $response = Invoke-CachedRestMethod -Uri $url -TimeoutSec $TimeoutSec -AsJson + return $response + } + catch { + Write-Warning "Failed to fetch Helix job ${JobId}: $_" + return $null + } +} + +function Get-HelixWorkItems { + param([string]$JobId) + + $url = "https://helix.dot.net/api/2019-06-17/jobs/$JobId/workitems" + + try { + $response = Invoke-CachedRestMethod -Uri $url -TimeoutSec $TimeoutSec -AsJson + return $response + } + catch { + Write-Warning "Failed to fetch work items for job ${JobId}: $_" + return $null + } +} + +function Get-HelixWorkItemFiles { + <# + .SYNOPSIS + Fetches work item files via the ListFiles endpoint which returns direct blob storage URIs. + .DESCRIPTION + Workaround for https://github.com/dotnet/dnceng/issues/6072: + The Details endpoint returns incorrect permalink URIs for files in subdirectories + and rejects unicode characters in filenames. The ListFiles endpoint returns direct + blob storage URIs that always work, regardless of subdirectory depth or unicode. + #> + param([string]$JobId, [string]$WorkItemName) + + $encodedWorkItem = [uri]::EscapeDataString($WorkItemName) + $url = "https://helix.dot.net/api/2019-06-17/jobs/$JobId/workitems/$encodedWorkItem/files" + + try { + $files = Invoke-CachedRestMethod -Uri $url -TimeoutSec $TimeoutSec -AsJson + return $files + } + catch { + Write-Warning "Failed to fetch files for work item ${WorkItemName}: $_" + return $null + } +} + +function Get-HelixWorkItemDetails { + param([string]$JobId, [string]$WorkItemName) + + $encodedWorkItem = [uri]::EscapeDataString($WorkItemName) + $url = "https://helix.dot.net/api/2019-06-17/jobs/$JobId/workitems/$encodedWorkItem" + + try { + $response = Invoke-CachedRestMethod -Uri $url -TimeoutSec $TimeoutSec -AsJson + + # Replace Files from the Details endpoint with results from ListFiles. + # The Details endpoint has broken URIs for subdirectory and unicode filenames + # (https://github.com/dotnet/dnceng/issues/6072). ListFiles returns direct + # blob storage URIs that always work. + $listFiles = Get-HelixWorkItemFiles -JobId $JobId -WorkItemName $WorkItemName + if ($null -ne $listFiles) { + $response.Files = @($listFiles | ForEach-Object { + [PSCustomObject]@{ + FileName = $_.Name + Uri = $_.Link + } + }) + } + + return $response + } + catch { + Write-Warning "Failed to fetch work item ${WorkItemName}: $_" + return $null + } +} + +function Get-HelixConsoleLog { + param([string]$Url) + + try { + $response = Invoke-CachedRestMethod -Uri $Url -TimeoutSec $TimeoutSec + return $response + } + catch { + Write-Warning "Failed to fetch Helix log from ${Url}: $_" + return $null + } +} + +function Find-WorkItemsWithBinlogs { + <# + .SYNOPSIS + Scans work items in a Helix job to find which ones contain binlog files. + .DESCRIPTION + Not all work items produce binlogs - only build/publish tests do. + This function helps locate work items that have binlogs for deeper analysis. + #> + param( + [Parameter(Mandatory)] + [string]$JobId, + [int]$MaxItems = 30, + [switch]$IncludeDetails + ) + + $workItems = Get-HelixWorkItems -JobId $JobId + if (-not $workItems) { + Write-Warning "No work items found for job $JobId" + return @() + } + + Write-Host "Scanning up to $MaxItems work items for binlogs..." -ForegroundColor Gray + + $results = @() + $scanned = 0 + + foreach ($wi in $workItems | Select-Object -First $MaxItems) { + $scanned++ + $details = Get-HelixWorkItemDetails -JobId $JobId -WorkItemName $wi.Name + if ($details -and $details.Files) { + $binlogs = @($details.Files | Where-Object { $_.FileName -like "*.binlog" }) + if ($binlogs.Count -gt 0) { + $result = @{ + Name = $wi.Name + BinlogCount = $binlogs.Count + Binlogs = $binlogs | ForEach-Object { $_.FileName } + ExitCode = $details.ExitCode + State = $details.State + } + if ($IncludeDetails) { + $result.BinlogUris = $binlogs | ForEach-Object { $_.Uri } + } + $results += $result + } + } + + # Progress indicator every 10 items + if ($scanned % 10 -eq 0) { + Write-Host " Scanned $scanned/$MaxItems..." -ForegroundColor DarkGray + } + } + + return $results +} + +#endregion Helix API Functions + +#region Output Formatting + +function Format-TestFailure { + param( + [string]$LogContent, + [int]$MaxLines = $MaxFailureLines, + [int]$MaxFailures = 3 + ) + + $lines = $LogContent -split "`n" + $allFailures = @() + $currentFailure = @() + $inFailure = $false + $emptyLineCount = 0 + $failureCount = 0 + + # Expanded failure detection patterns + # CAUTION: These trigger "failure block" capture. Overly broad patterns (e.g. \w+Error:) + # will grab Python harness/reporter noise and swamp the real test failure. + $failureStartPatterns = @( + '\[FAIL\]', + 'Assert\.\w+\(\)\s+Failure', + 'Expected:.*but was:', + 'BUG:', + 'FAILED\s*$', + 'END EXECUTION - FAILED', + 'System\.\w+Exception:', + 'Timed Out \(timeout' + ) + $combinedPattern = ($failureStartPatterns -join '|') + + foreach ($line in $lines) { + # Check for new failure start + if ($line -match $combinedPattern) { + # Save previous failure if exists + if ($currentFailure.Count -gt 0) { + $allFailures += ($currentFailure -join "`n") + $failureCount++ + if ($failureCount -ge $MaxFailures) { + break + } + } + # Start new failure + $currentFailure = @($line) + $inFailure = $true + $emptyLineCount = 0 + continue + } + + if ($inFailure) { + $currentFailure += $line + + # Track consecutive empty lines to detect end of stack trace + if ($line -match '^\s*$') { + $emptyLineCount++ + } + else { + $emptyLineCount = 0 + } + + # Stop this failure after stack trace ends (2+ consecutive empty lines) or max lines reached + if ($emptyLineCount -ge 2 -or $currentFailure.Count -ge $MaxLines) { + $allFailures += ($currentFailure -join "`n") + $currentFailure = @() + $inFailure = $false + $failureCount++ + if ($failureCount -ge $MaxFailures) { + break + } + } + } + } + + # Don't forget last failure + if ($currentFailure.Count -gt 0 -and $failureCount -lt $MaxFailures) { + $allFailures += ($currentFailure -join "`n") + } + + if ($allFailures.Count -eq 0) { + return $null + } + + $result = $allFailures -join "`n`n--- Next Failure ---`n`n" + + if ($failureCount -ge $MaxFailures) { + $result += "`n`n... (more failures exist, showing first $MaxFailures)" + } + + return $result +} + +# Helper to display test results from a test run +function Show-TestRunResults { + param( + [object[]]$TestRunUrls, + [string]$Org = "https://dev.azure.com/$Organization" + ) + + if (-not $TestRunUrls -or $TestRunUrls.Count -eq 0) { return } + + Write-Host "`n Test Results:" -ForegroundColor Yellow + foreach ($testRun in $TestRunUrls) { + Write-Host " Run $($testRun.RunId): $($testRun.Url)" -ForegroundColor Gray + + $testResults = Get-AzDOTestResults -RunId $testRun.RunId -Org $Org + if ($testResults -and $testResults.Count -gt 0) { + Write-Host "`n Failed tests ($($testResults.Count)):" -ForegroundColor Red + foreach ($result in $testResults | Select-Object -First 10) { + Write-Host " - $($result.name)" -ForegroundColor White + } + if ($testResults.Count -gt 10) { + Write-Host " ... and $($testResults.Count - 10) more" -ForegroundColor Gray + } + } + } +} + +#endregion Output Formatting + +#region Main Execution + +# Main execution +try { + # Handle direct Helix job query + if ($PSCmdlet.ParameterSetName -eq 'HelixJob') { + Write-Host "`n=== Helix Job $HelixJob ===" -ForegroundColor Yellow + Write-Host "URL: https://helix.dot.net/api/jobs/$HelixJob" -ForegroundColor Gray + + # Get job details + $jobDetails = Get-HelixJobDetails -JobId $HelixJob + if ($jobDetails) { + Write-Host "`nQueue: $($jobDetails.QueueId)" -ForegroundColor Cyan + Write-Host "Source: $($jobDetails.Source)" -ForegroundColor Cyan + } + + if ($WorkItem) { + # Query specific work item + Write-Host "`n--- Work Item: $WorkItem ---" -ForegroundColor Cyan + + $workItemDetails = Get-HelixWorkItemDetails -JobId $HelixJob -WorkItemName $WorkItem + if ($workItemDetails) { + Write-Host " State: $($workItemDetails.State)" -ForegroundColor $(if ($workItemDetails.State -eq 'Passed') { 'Green' } else { 'Red' }) + Write-Host " Exit Code: $($workItemDetails.ExitCode)" -ForegroundColor White + Write-Host " Machine: $($workItemDetails.MachineName)" -ForegroundColor Gray + Write-Host " Duration: $($workItemDetails.Duration)" -ForegroundColor Gray + + # Show artifacts with binlogs highlighted + if ($workItemDetails.Files -and $workItemDetails.Files.Count -gt 0) { + Write-Host "`n Artifacts:" -ForegroundColor Yellow + $binlogs = $workItemDetails.Files | Where-Object { $_.FileName -like "*.binlog" } + $otherFiles = $workItemDetails.Files | Where-Object { $_.FileName -notlike "*.binlog" } + + # Show binlogs first with special formatting + foreach ($file in $binlogs | Select-Object -Unique FileName, Uri) { + Write-Host " 📋 $($file.FileName): $($file.Uri)" -ForegroundColor Cyan + } + if ($binlogs.Count -gt 0) { + Write-Host " (Tip: Use MSBuild MCP server or https://live.msbuildlog.com/ to analyze binlogs)" -ForegroundColor DarkGray + } + + # Show other files + foreach ($file in $otherFiles | Select-Object -Unique FileName, Uri | Select-Object -First 10) { + Write-Host " $($file.FileName): $($file.Uri)" -ForegroundColor Gray + } + } + + # Fetch console log + $consoleUrl = "https://helix.dot.net/api/2019-06-17/jobs/$HelixJob/workitems/$WorkItem/console" + Write-Host "`n Console Log: $consoleUrl" -ForegroundColor Yellow + + $consoleLog = Get-HelixConsoleLog -Url $consoleUrl + if ($consoleLog) { + $failureInfo = Format-TestFailure -LogContent $consoleLog + if ($failureInfo) { + Write-Host $failureInfo -ForegroundColor White + + # Search for known issues + Show-KnownIssues -TestName $WorkItem -ErrorMessage $failureInfo -IncludeMihuBot:$SearchMihuBot + } + else { + # Show last 50 lines if no failure pattern detected + $lines = $consoleLog -split "`n" + $lastLines = $lines | Select-Object -Last 50 + Write-Host ($lastLines -join "`n") -ForegroundColor White + } + } + } + } + else { + # List all work items in the job + Write-Host "`nWork Items:" -ForegroundColor Yellow + $workItems = Get-HelixWorkItems -JobId $HelixJob + if ($workItems) { + Write-Host " Total: $($workItems.Count)" -ForegroundColor Cyan + Write-Host " Checking for failures..." -ForegroundColor Gray + + # Need to fetch details for each to find failures (list API only shows 'Finished') + $failedItems = @() + foreach ($wi in $workItems | Select-Object -First 20) { + $details = Get-HelixWorkItemDetails -JobId $HelixJob -WorkItemName $wi.Name + if ($details -and $null -ne $details.ExitCode -and $details.ExitCode -ne 0) { + $failedItems += @{ + Name = $wi.Name + ExitCode = $details.ExitCode + State = $details.State + } + } + } + + if ($failedItems.Count -gt 0) { + Write-Host "`n Failed Work Items:" -ForegroundColor Red + foreach ($wi in $failedItems | Select-Object -First $MaxJobs) { + Write-Host " - $($wi.Name) (Exit: $($wi.ExitCode))" -ForegroundColor White + } + Write-Host "`n Use -WorkItem '' to see details" -ForegroundColor Gray + } + else { + Write-Host " No failures found in first 20 work items" -ForegroundColor Green + } + + Write-Host "`n All work items:" -ForegroundColor Yellow + foreach ($wi in $workItems | Select-Object -First 10) { + Write-Host " - $($wi.Name)" -ForegroundColor White + } + if ($workItems.Count -gt 10) { + Write-Host " ... and $($workItems.Count - 10) more" -ForegroundColor Gray + } + + # Find work items with binlogs if requested + if ($FindBinlogs) { + Write-Host "`n === Binlog Search ===" -ForegroundColor Yellow + $binlogResults = Find-WorkItemsWithBinlogs -JobId $HelixJob -MaxItems 30 -IncludeDetails + + if ($binlogResults.Count -gt 0) { + Write-Host "`n Work items with binlogs:" -ForegroundColor Cyan + foreach ($result in $binlogResults) { + $stateColor = if ($result.ExitCode -eq 0) { 'Green' } else { 'Red' } + Write-Host " $($result.Name)" -ForegroundColor $stateColor + Write-Host " Binlogs ($($result.BinlogCount)):" -ForegroundColor Gray + foreach ($binlog in $result.Binlogs | Select-Object -First 5) { + Write-Host " - $binlog" -ForegroundColor White + } + if ($result.Binlogs.Count -gt 5) { + Write-Host " ... and $($result.Binlogs.Count - 5) more" -ForegroundColor DarkGray + } + } + Write-Host "`n Tip: Use -WorkItem '' to get full binlog URIs" -ForegroundColor DarkGray + } + else { + Write-Host " No binlogs found in scanned work items." -ForegroundColor Yellow + Write-Host " This job may contain only unit tests (which don't produce binlogs)." -ForegroundColor Gray + } + } + } + } + + exit 0 + } + + # Get build ID(s) if using PR number + $buildIds = @() + $knownIssuesFromBuildAnalysis = @() + $prChangedFiles = @() + $noBuildReason = $null + if ($PSCmdlet.ParameterSetName -eq 'PRNumber') { + $buildResult = Get-AzDOBuildIdFromPR -PR $PRNumber + if ($buildResult.Reason) { + # No builds found — emit summary with reason and exit + $noBuildReason = $buildResult.Reason + $buildIds = @() + $summary = [ordered]@{ + mode = "PRNumber" + repository = $Repository + prNumber = $PRNumber + builds = @() + totalFailedJobs = 0 + totalLocalFailures = 0 + lastBuildJobSummary = [ordered]@{ + total = 0; succeeded = 0; failed = 0; canceled = 0; pending = 0; warnings = 0; skipped = 0 + } + failedJobNames = @() + failedJobDetails = @() + canceledJobNames = @() + knownIssues = @() + prCorrelation = [ordered]@{ + changedFileCount = 0 + hasCorrelation = $false + correlatedFiles = @() + } + recommendationHint = if ($noBuildReason -eq "MERGE_CONFLICTS") { "MERGE_CONFLICTS" } else { "NO_BUILDS" } + noBuildReason = $noBuildReason + mergeState = $buildResult.MergeState + } + Write-Host "" + Write-Host "[CI_ANALYSIS_SUMMARY]" + Write-Host ($summary | ConvertTo-Json -Depth 5) + Write-Host "[/CI_ANALYSIS_SUMMARY]" + exit 0 + } + $buildIds = @($buildResult.BuildIds) + + # Check Build Analysis for known issues + $knownIssuesFromBuildAnalysis = @(Get-BuildAnalysisKnownIssues -PR $PRNumber) + + # Get changed files for correlation + $prChangedFiles = @(Get-PRChangedFiles -PR $PRNumber) + if ($prChangedFiles.Count -gt 0) { + Write-Verbose "PR has $($prChangedFiles.Count) changed files" + } + } + else { + $buildIds = @($BuildId) + } + + # Process each build + $totalFailedJobs = 0 + $totalLocalFailures = 0 + $allFailuresForCorrelation = @() + $allFailedJobNames = @() + $allCanceledJobNames = @() + $allFailedJobDetails = @() + $lastBuildJobSummary = $null + + foreach ($currentBuildId in $buildIds) { + Write-Host "`n=== Azure DevOps Build $currentBuildId ===" -ForegroundColor Yellow + Write-Host "URL: https://dev.azure.com/$Organization/$Project/_build/results?buildId=$currentBuildId" -ForegroundColor Gray + + # Get and display build status + $buildStatus = Get-AzDOBuildStatus -Build $currentBuildId + if ($buildStatus) { + $statusColor = switch ($buildStatus.Status) { + "inProgress" { "Cyan" } + "completed" { if ($buildStatus.Result -eq "succeeded") { "Green" } else { "Red" } } + default { "Gray" } + } + $statusText = $buildStatus.Status + if ($buildStatus.Status -eq "completed" -and $buildStatus.Result) { + $statusText = "$($buildStatus.Status) ($($buildStatus.Result))" + } + elseif ($buildStatus.Status -eq "inProgress") { + $statusText = "IN PROGRESS - showing failures so far" + } + Write-Host "Status: $statusText" -ForegroundColor $statusColor + } + + # Get timeline + $isInProgress = $buildStatus -and $buildStatus.Status -eq "inProgress" + $timeline = Get-AzDOTimeline -Build $currentBuildId -BuildInProgress:$isInProgress + + # Handle timeline fetch failure + if (-not $timeline) { + Write-Host "`nCould not fetch build timeline" -ForegroundColor Red + Write-Host "Build URL: https://dev.azure.com/$Organization/$Project/_build/results?buildId=$currentBuildId" -ForegroundColor Gray + continue + } + + # Get failed jobs + $failedJobs = Get-FailedJobs -Timeline $timeline + + # Get canceled jobs (different from failed - typically due to dependency failures) + $canceledJobs = Get-CanceledJobs -Timeline $timeline + + # Also check for local test failures (non-Helix) + $localTestFailures = Get-LocalTestFailures -Timeline $timeline -BuildId $currentBuildId + + # Accumulate totals and compute job summary BEFORE any continue branches + $totalFailedJobs += $failedJobs.Count + $totalLocalFailures += $localTestFailures.Count + $allFailedJobNames += @($failedJobs | ForEach-Object { $_.name }) + $allCanceledJobNames += @($canceledJobs | ForEach-Object { $_.name }) + + $allJobs = @() + $succeededJobs = 0 + $pendingJobs = 0 + $canceledJobCount = 0 + $skippedJobs = 0 + $warningJobs = 0 + if ($timeline -and $timeline.records) { + $allJobs = @($timeline.records | Where-Object { $_.type -eq "Job" }) + $succeededJobs = @($allJobs | Where-Object { $_.result -eq "succeeded" }).Count + $warningJobs = @($allJobs | Where-Object { $_.result -eq "succeededWithIssues" }).Count + $pendingJobs = @($allJobs | Where-Object { -not $_.result -or $_.state -eq "pending" -or $_.state -eq "inProgress" }).Count + $canceledJobCount = @($allJobs | Where-Object { $_.result -eq "canceled" }).Count + $skippedJobs = @($allJobs | Where-Object { $_.result -eq "skipped" }).Count + } + $lastBuildJobSummary = [ordered]@{ + total = $allJobs.Count + succeeded = $succeededJobs + failed = if ($failedJobs) { $failedJobs.Count } else { 0 } + canceled = $canceledJobCount + pending = $pendingJobs + warnings = $warningJobs + skipped = $skippedJobs + } + + if ((-not $failedJobs -or $failedJobs.Count -eq 0) -and $localTestFailures.Count -eq 0) { + if ($buildStatus -and $buildStatus.Status -eq "inProgress") { + Write-Host "`nNo failures yet - build still in progress" -ForegroundColor Cyan + Write-Host "Run again later to check for failures, or use -NoCache to get fresh data" -ForegroundColor Gray + } + else { + Write-Host "`nNo failed jobs found in build $currentBuildId" -ForegroundColor Green + } + # Still show canceled jobs if any + if ($canceledJobs -and $canceledJobs.Count -gt 0) { + Write-Host "`nNote: $($canceledJobs.Count) job(s) were canceled (not failed):" -ForegroundColor DarkYellow + foreach ($job in $canceledJobs | Select-Object -First 5) { + Write-Host " - $($job.name)" -ForegroundColor DarkGray + } + if ($canceledJobs.Count -gt 5) { + Write-Host " ... and $($canceledJobs.Count - 5) more" -ForegroundColor DarkGray + } + Write-Host " (Canceled jobs are typically due to earlier stage failures or timeouts)" -ForegroundColor DarkGray + } + continue + } + + # Report local test failures first (these may exist even without failed jobs) + if ($localTestFailures.Count -gt 0) { + Write-Host "`n=== Local Test Failures (non-Helix) ===" -ForegroundColor Yellow + Write-Host "Build: https://dev.azure.com/$Organization/$Project/_build/results?buildId=$currentBuildId" -ForegroundColor Gray + + foreach ($failure in $localTestFailures) { + Write-Host "`n--- $($failure.TaskName) ---" -ForegroundColor Cyan + + # Collect issues for correlation + $issueMessages = $failure.Issues | ForEach-Object { $_.message } + $allFailuresForCorrelation += @{ + TaskName = $failure.TaskName + JobName = "Local Test" + Errors = $issueMessages + HelixLogs = @() + FailedTests = @() + } + + # Show build and log links + $jobLogUrl = "https://dev.azure.com/$Organization/$Project/_build/results?buildId=$currentBuildId&view=logs&j=$($failure.ParentJobId)" + if ($failure.TaskId) { + $jobLogUrl += "&t=$($failure.TaskId)" + } + Write-Host " Log: $jobLogUrl" -ForegroundColor Gray + + # Show issues + foreach ($issue in $failure.Issues) { + Write-Host " $($issue.message)" -ForegroundColor Red + } + + # Show test run URLs if available + if ($failure.TestRunUrls.Count -gt 0) { + Show-TestRunResults -TestRunUrls $failure.TestRunUrls -Org "https://dev.azure.com/$Organization" + } + + # Try to get more details from the task log + if ($failure.LogId) { + $logContent = Get-BuildLog -Build $currentBuildId -LogId $failure.LogId + if ($logContent) { + # Extract test run URLs from this log too + $additionalRuns = Extract-TestRunUrls -LogContent $logContent + if ($additionalRuns.Count -gt 0 -and $failure.TestRunUrls.Count -eq 0) { + Show-TestRunResults -TestRunUrls $additionalRuns -Org "https://dev.azure.com/$Organization" + } + + # Search for known issues based on build errors and task name + $buildErrors = Extract-BuildErrors -LogContent $logContent + if ($buildErrors.Count -gt 0) { + Show-KnownIssues -ErrorMessage ($buildErrors -join "`n") -IncludeMihuBot:$SearchMihuBot + } + elseif ($failure.TaskName) { + # If no specific errors, try searching by task name + Show-KnownIssues -TestName $failure.TaskName -IncludeMihuBot:$SearchMihuBot + } + } + } + } + } + + if (-not $failedJobs -or $failedJobs.Count -eq 0) { + Write-Host "`n=== Summary ===" -ForegroundColor Yellow + Write-Host "Local test failures: $($localTestFailures.Count)" -ForegroundColor Red + Write-Host "Build URL: https://dev.azure.com/$Organization/$Project/_build/results?buildId=$currentBuildId" -ForegroundColor Cyan + continue + } + + Write-Host "`nFound $($failedJobs.Count) failed job(s):" -ForegroundColor Red + + # Show canceled jobs if any (these are different from failed) + if ($canceledJobs -and $canceledJobs.Count -gt 0) { + Write-Host "Also $($canceledJobs.Count) job(s) were canceled (due to earlier failures/timeouts):" -ForegroundColor DarkYellow + foreach ($job in $canceledJobs | Select-Object -First 3) { + Write-Host " - $($job.name)" -ForegroundColor DarkGray + } + if ($canceledJobs.Count -gt 3) { + Write-Host " ... and $($canceledJobs.Count - 3) more" -ForegroundColor DarkGray + } + } + + $processedJobs = 0 + $errorCount = 0 + foreach ($job in $failedJobs) { + if ($processedJobs -ge $MaxJobs) { + Write-Host "`n... and $($failedJobs.Count - $MaxJobs) more failed jobs (use -MaxJobs to see more)" -ForegroundColor Yellow + break + } + + try { + Write-Host "`n--- $($job.name) ---" -ForegroundColor Cyan + Write-Host " Build: https://dev.azure.com/$Organization/$Project/_build/results?buildId=$currentBuildId&view=logs&j=$($job.id)" -ForegroundColor Gray + + # Track per-job failure details for JSON summary + $jobDetail = [ordered]@{ + jobName = $job.name + buildId = $currentBuildId + errorSnippet = "" + helixWorkItems = @() + errorCategory = "unclassified" + } + + # Get Helix tasks for this job + $helixTasks = Get-HelixJobInfo -Timeline $timeline -JobId $job.id + + if ($helixTasks) { + foreach ($task in $helixTasks) { + if ($task.log) { + Write-Host " Fetching Helix task log..." -ForegroundColor Gray + $logContent = Get-BuildLog -Build $currentBuildId -LogId $task.log.id + + if ($logContent) { + # Extract test failures + $failures = Extract-TestFailures -LogContent $logContent + + if ($failures.Count -gt 0) { + Write-Host " Failed tests:" -ForegroundColor Red + foreach ($failure in $failures) { + Write-Host " - $($failure.TestName)" -ForegroundColor White + } + + # Collect for PR correlation + $allFailuresForCorrelation += @{ + TaskName = $task.name + JobName = $job.name + Errors = @() + HelixLogs = @() + FailedTests = $failures | ForEach-Object { $_.TestName } + } + $jobDetail.errorCategory = "test-failure" + $jobDetail.errorSnippet = ($failures | Select-Object -First 3 | ForEach-Object { $_.TestName }) -join "; " + } + + # Extract and optionally fetch Helix URLs + $helixUrls = Extract-HelixUrls -LogContent $logContent + + if ($helixUrls.Count -gt 0 -and $ShowLogs) { + Write-Host "`n Helix Console Logs:" -ForegroundColor Yellow + + foreach ($url in $helixUrls | Select-Object -First 3) { + Write-Host "`n $url" -ForegroundColor Gray + + # Extract work item name from URL for known issue search + $workItemName = "" + if ($url -match '/workitems/([^/]+)/console') { + $workItemName = $Matches[1] + $jobDetail.helixWorkItems += $workItemName + } + + $helixLog = Get-HelixConsoleLog -Url $url + if ($helixLog) { + $failureInfo = Format-TestFailure -LogContent $helixLog + if ($failureInfo) { + Write-Host $failureInfo -ForegroundColor White + + # Categorize failure from log content + if ($failureInfo -match 'Timed Out \(timeout') { + $jobDetail.errorCategory = "test-timeout" + } elseif ($failureInfo -match 'Exit Code:\s*(139|134|-4)' -or $failureInfo -match 'createdump') { + # Crash takes highest precedence — don't downgrade + if ($jobDetail.errorCategory -notin @("crash")) { + $jobDetail.errorCategory = "crash" + } + } elseif ($failureInfo -match 'Traceback \(most recent call last\)' -and $helixLog -match 'Tests run:.*Failures:\s*0') { + # Work item failed (non-zero exit from reporter crash) but all tests passed. + # The Python traceback is from Helix infrastructure, not from the test itself. + if ($jobDetail.errorCategory -notin @("crash", "test-timeout")) { + $jobDetail.errorCategory = "tests-passed-reporter-failed" + } + } elseif ($jobDetail.errorCategory -eq "unclassified") { + $jobDetail.errorCategory = "test-failure" + } + if (-not $jobDetail.errorSnippet) { + $jobDetail.errorSnippet = $failureInfo.Substring(0, [Math]::Min(200, $failureInfo.Length)) + } + + # Search for known issues + Show-KnownIssues -TestName $workItemName -ErrorMessage $failureInfo -IncludeMihuBot:$SearchMihuBot + } + else { + # No failure pattern matched — show tail of log + $lines = $helixLog -split "`n" + $lastLines = $lines | Select-Object -Last 20 + $tailText = $lastLines -join "`n" + Write-Host $tailText -ForegroundColor White + if (-not $jobDetail.errorSnippet) { + $jobDetail.errorSnippet = $tailText.Substring(0, [Math]::Min(200, $tailText.Length)) + } + Show-KnownIssues -TestName $workItemName -ErrorMessage $tailText -IncludeMihuBot:$SearchMihuBot + } + } + } + } + elseif ($helixUrls.Count -gt 0) { + Write-Host "`n Helix logs available (use -ShowLogs to fetch):" -ForegroundColor Yellow + foreach ($url in $helixUrls | Select-Object -First 3) { + Write-Host " $url" -ForegroundColor Gray + } + } + } + } + } + } + else { + # No Helix tasks - this is a build failure, extract actual errors + $buildTasks = $timeline.records | Where-Object { + $_.parentId -eq $job.id -and $_.result -eq "failed" + } + + foreach ($task in $buildTasks | Select-Object -First 3) { + Write-Host " Failed task: $($task.name)" -ForegroundColor Red + + # Fetch and parse the build log for actual errors + if ($task.log) { + $logUrl = "https://dev.azure.com/$Organization/$Project/_build/results?buildId=$currentBuildId&view=logs&j=$($job.id)&t=$($task.id)" + Write-Host " Log: $logUrl" -ForegroundColor Gray + $logContent = Get-BuildLog -Build $currentBuildId -LogId $task.log.id + + if ($logContent) { + $buildErrors = Extract-BuildErrors -LogContent $logContent + + if ($buildErrors.Count -gt 0) { + # Collect for PR correlation + $allFailuresForCorrelation += @{ + TaskName = $task.name + JobName = $job.name + Errors = $buildErrors + HelixLogs = @() + FailedTests = @() + } + $jobDetail.errorCategory = "build-error" + if (-not $jobDetail.errorSnippet) { + $snippet = ($buildErrors | Select-Object -First 2) -join "; " + $jobDetail.errorSnippet = $snippet.Substring(0, [Math]::Min(200, $snippet.Length)) + } + + # Extract Helix log URLs from the full log content + $helixLogUrls = Extract-HelixLogUrls -LogContent $logContent + + if ($helixLogUrls.Count -gt 0) { + Write-Host " Helix failures ($($helixLogUrls.Count)):" -ForegroundColor Red + foreach ($helixLog in $helixLogUrls | Select-Object -First 5) { + Write-Host " - $($helixLog.WorkItem)" -ForegroundColor White + Write-Host " Log: $($helixLog.Url)" -ForegroundColor Gray + } + if ($helixLogUrls.Count -gt 5) { + Write-Host " ... and $($helixLogUrls.Count - 5) more" -ForegroundColor Gray + } + } + else { + Write-Host " Build errors:" -ForegroundColor Red + foreach ($err in $buildErrors | Select-Object -First 5) { + Write-Host " $err" -ForegroundColor White + } + if ($buildErrors.Count -gt 5) { + Write-Host " ... and $($buildErrors.Count - 5) more errors" -ForegroundColor Gray + } + } + + # Search for known issues + Show-KnownIssues -ErrorMessage ($buildErrors -join "`n") -IncludeMihuBot:$SearchMihuBot + } + else { + Write-Host " (No specific errors extracted from log)" -ForegroundColor Gray + } + } + } + } + } + + $allFailedJobDetails += $jobDetail + $processedJobs++ + } + catch { + $errorCount++ + if ($ContinueOnError) { + Write-Warning " Error processing job '$($job.name)': $_" + } + else { + throw [System.Exception]::new("Error processing job '$($job.name)': $($_.Exception.Message)", $_.Exception) + } + } + } + + Write-Host "`n=== Build $currentBuildId Summary ===" -ForegroundColor Yellow + if ($allJobs.Count -gt 0) { + $parts = @() + if ($succeededJobs -gt 0) { $parts += "$succeededJobs passed" } + if ($warningJobs -gt 0) { $parts += "$warningJobs passed with warnings" } + if ($failedJobs.Count -gt 0) { $parts += "$($failedJobs.Count) failed" } + if ($canceledJobCount -gt 0) { $parts += "$canceledJobCount canceled" } + if ($skippedJobs -gt 0) { $parts += "$skippedJobs skipped" } + if ($pendingJobs -gt 0) { $parts += "$pendingJobs pending" } + $jobSummary = $parts -join ", " + $allSucceeded = ($failedJobs.Count -eq 0 -and $pendingJobs -eq 0 -and $canceledJobCount -eq 0 -and ($succeededJobs + $warningJobs + $skippedJobs) -eq $allJobs.Count) + $summaryColor = if ($allSucceeded) { "Green" } elseif ($failedJobs.Count -gt 0) { "Red" } else { "Cyan" } + Write-Host "Jobs: $($allJobs.Count) total ($jobSummary)" -ForegroundColor $summaryColor + } + else { + Write-Host "Failed jobs: $($failedJobs.Count)" -ForegroundColor Red + } + if ($localTestFailures.Count -gt 0) { + Write-Host "Local test failures: $($localTestFailures.Count)" -ForegroundColor Red + } + if ($errorCount -gt 0) { + Write-Host "API errors (partial results): $errorCount" -ForegroundColor Yellow + } + Write-Host "Build URL: https://dev.azure.com/$Organization/$Project/_build/results?buildId=$currentBuildId" -ForegroundColor Cyan +} + +# Show PR change correlation if we have changed files +if ($prChangedFiles.Count -gt 0 -and $allFailuresForCorrelation.Count -gt 0) { + Show-PRCorrelationSummary -ChangedFiles $prChangedFiles -AllFailures $allFailuresForCorrelation +} + +# Overall summary if multiple builds +if ($buildIds.Count -gt 1) { + Write-Host "`n=== Overall Summary ===" -ForegroundColor Magenta + Write-Host "Analyzed $($buildIds.Count) builds" -ForegroundColor White + Write-Host "Total failed jobs: $totalFailedJobs" -ForegroundColor Red + Write-Host "Total local test failures: $totalLocalFailures" -ForegroundColor Red + + if ($knownIssuesFromBuildAnalysis.Count -gt 0) { + Write-Host "`nKnown Issues (from Build Analysis):" -ForegroundColor Yellow + foreach ($issue in $knownIssuesFromBuildAnalysis) { + Write-Host " - #$($issue.Number): $($issue.Title)" -ForegroundColor Gray + Write-Host " $($issue.Url)" -ForegroundColor DarkGray + } + } +} + +# Build structured summary and emit as JSON +$summary = [ordered]@{ + mode = $PSCmdlet.ParameterSetName + repository = $Repository + prNumber = if ($PSCmdlet.ParameterSetName -eq 'PRNumber') { $PRNumber } else { $null } + builds = @($buildIds | ForEach-Object { + [ordered]@{ + buildId = $_ + url = "https://dev.azure.com/$Organization/$Project/_build/results?buildId=$_" + } + }) + totalFailedJobs = $totalFailedJobs + totalLocalFailures = $totalLocalFailures + lastBuildJobSummary = if ($lastBuildJobSummary) { $lastBuildJobSummary } else { [ordered]@{ + total = 0; succeeded = 0; failed = 0; canceled = 0; pending = 0; warnings = 0; skipped = 0 + } } + failedJobNames = @($allFailedJobNames) + failedJobDetails = @($allFailedJobDetails) + failedJobDetailsTruncated = ($allFailedJobNames.Count -gt $allFailedJobDetails.Count) + canceledJobNames = @($allCanceledJobNames) + knownIssues = @($knownIssuesFromBuildAnalysis | ForEach-Object { + [ordered]@{ number = $_.Number; title = $_.Title; url = $_.Url } + }) + prCorrelation = [ordered]@{ + changedFileCount = $prChangedFiles.Count + hasCorrelation = $false + correlatedFiles = @() + } + recommendationHint = "" +} + +# Compute PR correlation using shared helper +if ($prChangedFiles.Count -gt 0 -and $allFailuresForCorrelation.Count -gt 0) { + $correlation = Get-PRCorrelation -ChangedFiles $prChangedFiles -AllFailures $allFailuresForCorrelation + $allCorrelated = @($correlation.CorrelatedFiles) + @($correlation.TestFiles) | Select-Object -Unique + $summary.prCorrelation.hasCorrelation = $allCorrelated.Count -gt 0 + $summary.prCorrelation.correlatedFiles = @($allCorrelated) +} + +# Compute recommendation hint +# Priority: KNOWN_ISSUES wins over LIKELY_PR_RELATED intentionally. +# When both exist, SKILL.md "Mixed signals" guidance tells the agent to separate them. +if (-not $lastBuildJobSummary -and $buildIds.Count -gt 0) { + $summary.recommendationHint = "REVIEW_REQUIRED" +} elseif ($knownIssuesFromBuildAnalysis.Count -gt 0) { + $summary.recommendationHint = "KNOWN_ISSUES_DETECTED" +} elseif ($totalFailedJobs -eq 0 -and $totalLocalFailures -eq 0) { + $summary.recommendationHint = "BUILD_SUCCESSFUL" +} elseif ($summary.prCorrelation.hasCorrelation) { + $summary.recommendationHint = "LIKELY_PR_RELATED" +} elseif ($prChangedFiles.Count -gt 0 -and $allFailuresForCorrelation.Count -gt 0) { + $summary.recommendationHint = "POSSIBLY_TRANSIENT" +} else { + $summary.recommendationHint = "REVIEW_REQUIRED" +} + +Write-Host "" +Write-Host "[CI_ANALYSIS_SUMMARY]" +Write-Host ($summary | ConvertTo-Json -Depth 5) +Write-Host "[/CI_ANALYSIS_SUMMARY]" + +} +catch { + Write-Error "Error: $_" + exit 1 +} + +#endregion Main Execution diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md new file mode 100644 index 000000000000..7b3de1150d5a --- /dev/null +++ b/.github/skills/code-review/SKILL.md @@ -0,0 +1,685 @@ +--- +name: code-review +description: Review code changes in dotnet/runtime for correctness, performance, and consistency with project conventions. Use when reviewing PRs or code changes. +--- + +# dotnet/runtime Code Review + +Review code changes against conventions and patterns established by dotnet/runtime maintainers. These rules were extracted from 43,000+ maintainer review comments across 6,600+ PRs and represent the actual standards enforced in practice. + +**Reviewer mindset:** Be polite but very skeptical. Your job is to help speed the review process for maintainers, which includes not only finding problems the PR author may have missed but also questioning the value of the PR in its entirety. Treat the PR description and linked issues as claims to verify, not facts to accept. Question the stated direction, probe edge cases, and don't hesitate to flag concerns even when unsure. + +## When to Use This Skill + +Use this skill when: +- Reviewing a PR or code change in dotnet/runtime +- Checking code for correctness, performance, style, or consistency issues before submitting a PR +- Asked to review, critique, or provide feedback on code changes +- Validating that a change follows dotnet/runtime conventions + +## Review Process + +### Step 0: Gather Code Context (No PR Narrative Yet) + +Before analyzing anything, collect as much relevant **code** context as you can. **Critically, do NOT read the PR description, linked issues, or existing review comments yet.** You must form your own independent assessment of what the code does, why it might be needed, what problems it has, and whether the approach is sound — before being exposed to the author's framing. Reading the author's narrative first anchors your judgment and makes you less likely to find real problems. + +1. **Diff and file list**: Fetch the full diff and the list of changed files. +2. **Full source files**: For every changed file, read the **entire source file** (not just the diff hunks). You need the surrounding code to understand invariants, locking protocols, call patterns, and data flow. Diff-only review is the #1 cause of false positives and missed issues. +3. **Consumers and callers**: If the change modifies a public/internal API, a type that others depend on, or a virtual/interface method, search for how consumers use the functionality. Grep for callers, usages, and test sites. Understanding how the code is consumed reveals whether the change could break existing behavior or violate caller assumptions. +4. **Sibling types and related code**: If the change fixes a bug or adds a pattern in one type, check whether sibling types (e.g., other abstraction implementations, other collection types, platform-specific variants) have the same issue or need the same fix. Fetch and read those files too. +5. **Key utility/helper files**: If the diff calls into shared utilities, read those to understand the contracts (thread-safety, idempotency, etc.). +6. **Git history**: Check recent commits to the changed files (`git log --oneline -20 -- `). Look for related recent changes, reverts, or prior attempts to fix the same problem. This reveals whether the area is actively churning, whether a similar fix was tried and reverted, or whether the current change conflicts with recent work. + +### Step 1: Form an Independent Assessment + +Based **only** on the code context gathered above (without the PR description or issue), answer these questions: + +1. **What does this change actually do?** Describe the behavioral change in your own words by reading the diff and surrounding code. What was the old behavior? What is the new behavior? +2. **Why might this change be needed?** Infer the motivation from the code itself. What bug, gap, or improvement does it appear to address? +3. **Is this the right approach?** Would a simpler alternative be more consistent with the codebase? Could the goal be achieved with existing functionality? Are there correctness, performance, or safety concerns? +4. **What problems do you see?** Identify bugs, edge cases, missing validation, thread-safety issues, performance regressions, API design problems, test gaps, and anything else that concerns you. + +Write down your independent assessment before proceeding. You must produce a holistic assessment (see [Holistic PR Assessment](#holistic-pr-assessment)) at this stage. + +### Step 2: Incorporate PR Narrative and Reconcile + +Now read the PR description, labels, linked issues (in full), author information, existing review comments, and any related open issues in the same area. Treat all of this as **claims to verify**, not facts to accept. + +1. **PR metadata**: Fetch the PR description, labels, linked issues, and author. Read linked issues in full — they often contain the repro, root cause analysis, and constraints the fix must satisfy. +2. **Related issues**: Search for other open issues in the same area (same labels, same component). This can reveal known problems the PR should also address, or constraints the author may not be aware of. +3. **Existing review comments**: Check if there are already review comments on the PR to avoid duplicating feedback. +4. **Reconcile your assessment with the author's claims.** Where your independent reading of the code disagrees with the PR description or issue, investigate further — but do not simply defer to the author's framing. If the PR claims a bug fix, a performance improvement, or a behavioral correction, verify those claims against the code and any provided evidence. If your independent assessment found problems the PR narrative doesn't acknowledge, those problems are more likely to be real, not less. +5. **Update your holistic assessment** if the additional context reveals information that genuinely changes your evaluation (e.g., a linked issue proves the bug is real, or an existing review comment already identified the same concern). But do not soften findings just because the PR description sounds reasonable. + +### Step 3: Detailed Analysis + +1. **Focus on what matters.** Prioritize bugs, performance regressions, safety issues, race conditions, resource management problems, incorrect assumptions about data or state, and API design problems. Do not comment on trivial style issues unless they violate an explicit rule below. +2. **Consider collateral damage.** For every changed code path, actively brainstorm: what other scenarios, callers, or inputs flow through this code? Could any of them break or behave differently after this change? If you identify any plausible risk — even one you can't fully confirm — surface it so the author can evaluate. Do not dismiss behavioral changes because you believe the fix justifies them. The tradeoff is the author's decision — your job is to make it visible. +3. **Be specific and actionable.** Every comment should tell the author exactly what to change and why. Reference the relevant convention. Include evidence of how you verified the issue is real, e.g., "looked at all callers and none of them validate this parameter". +4. **Flag severity clearly:** + - ❌ **error** — Must fix before merge. Bugs, security issues, API violations, test gaps for behavior changes. + - ⚠️ **warning** — Should fix. Performance issues, missing validation, inconsistency with established patterns. + - 💡 **suggestion** — Consider changing. Style improvements, minor readability wins, optional optimizations. +5. **Don't pile on.** If the same issue appears many times, flag it once on the primary file with a note listing all affected files. Do not leave separate comments for each occurrence. +6. **Respect existing style.** When modifying existing files, the file's current style takes precedence over general guidelines. +7. **Don't flag what CI catches.** Do not flag issues that a linter, typechecker, compiler, analyzer, or CI build step would catch, e.g., missing usings, unsupported syntax, formatting. Assume CI will run separately. +8. **Avoid false positives.** Before flagging any issue: + - **Verify the concern actually applies** given the full context, not just the diff. Open the surrounding code to check. Confirm the issue isn't already handled by a caller, callee, or wrapper layer before claiming something is missing. + - **Skip theoretical concerns with negligible real-world probability.** "Could happen" is not the same as "will happen." + - **If you're unsure, either investigate further until you're confident, or surface it explicitly as a low-confidence question rather than a firm claim.** Do not speculate about issues you have no concrete basis for. Every comment should be worth the reader's time. + - **Trust the author's context.** The author knows their codebase. If a pattern seems odd but is consistent with the repo, assume it's intentional. + - **Never assert that something "does not exist," "is deprecated," or "is unavailable" based on training data alone.** Your knowledge has a cutoff date. When uncertain, ask rather than assert. +9. **Ensure code suggestions are valid.** Any code you suggest must be syntactically correct and complete. Ensure any suggestion would result in working code. +10. **Label in-scope vs. follow-up.** Distinguish between issues the PR should fix and out-of-scope improvements. Be explicit when a suggestion is a follow-up rather than a blocker. + +## Multi-Model Review + +When the environment supports launching sub-agents with different models (e.g., the `task` tool with a `model` parameter), run the review in parallel across multiple model families to get diverse perspectives. Different models catch different classes of issues. If the environment does not support this, proceed with a single-model review. + +**How to execute (when supported):** +1. Inspect the available model list and select one model from each distinct model family (e.g., one Anthropic Claude, one Google Gemini, one OpenAI GPT). Use at least 2 and at most 4 models. **Model selection rules:** + - Pick only from models explicitly listed as available in the environment. Do not guess or assume model names. + - From each family, pick the model with the highest capability tier (prefer "premium" or "standard" over "fast/cheap"). + - Never pick models labeled "mini", "fast", or "cheap" for code review. + - If multiple standard-tier models exist in the same family (e.g., `gpt-5` and `gpt-5.1`), pick the one with the highest version number. + - Do not select the same model that is already running the primary review (i.e., your own model). The goal is diverse perspectives from different model families. +2. Launch a sub-agent for each selected model in parallel, giving each the same review prompt: the PR diff, the review rules from this skill, and instructions to produce findings in the severity format defined above. +3. Wait for all agents to complete, then synthesize: deduplicate findings that appear across models, elevate issues flagged by multiple models (higher confidence), and include unique findings from individual models that meet the confidence bar. **Timeout handling:** If a sub-agent has not completed after 10 minutes and you have results from other agents, proceed with the results you have. Do not block the review indefinitely waiting for a single slow model. Note in the output which models contributed. +4. Present a single unified review to the user, noting when an issue was flagged by multiple models. + +--- + +## Review Output Format + +When presenting the final review (whether as a PR comment or as output to the user), use the following structure. This ensures consistency across reviews and makes the output easy to scan. + +### Structure + +``` +## 🤖 Copilot Code Review — PR # + +### Holistic Assessment + +**Motivation**: <1-2 sentences on whether the PR is justified and the problem is real> + +**Approach**: <1-2 sentences on whether the fix/change takes the right approach> + +**Summary**: <✅ LGTM / ⚠️ Needs Human Review / ⚠️ Needs Changes / ❌ Reject>. <2-3 sentence summary of the overall verdict and key points. If "Needs Human Review," explicitly state which findings you are uncertain about and what a human reviewer should focus on.> + +--- + +### Detailed Findings + +#### ✅/⚠️/❌ + + + +(Repeat for each finding category. Group related findings under a single heading.) +``` + +### Guidelines + +- **Holistic Assessment** comes first and covers Motivation, Approach, and Summary. +- **Detailed Findings** uses emoji-prefixed category headers: + - ✅ for things that are correct / look good (use to confirm important aspects were verified) + - ⚠️ for warnings or impactful suggestions (should fix, or follow-up) + - ❌ for errors (must fix before merge) + - 💡 for minor suggestions or observations (nice-to-have) +- **Cross-cutting analysis** should be included when relevant: check whether related code (sibling types, callers, other platforms) is affected by the same issue or needs a similar fix. +- **Test quality** should be assessed as its own finding when tests are part of the PR. +- **Summary** gives a clear verdict: LGTM (no blocking issues — use only when confident), Needs Human Review (code may be correct but you have unresolved concerns or uncertainty that require human judgment), Needs Changes (with blocking issues listed), or Reject (explaining why this should be closed outright). **Never give a blanket LGTM when you are unsure.** When in doubt, use "Needs Human Review" and explain what a human should focus on. +- Keep the review concise but thorough. Every claim should be backed by evidence from the code. + +### Verdict Consistency Rules + +The summary verdict **must** be consistent with the findings in the body. Follow these rules: + +1. **The verdict must reflect your most severe finding.** If you have any ⚠️ findings, the verdict cannot be "LGTM." Use "Needs Human Review" or "Needs Changes" instead. Only use "LGTM" when all findings are ✅ or 💡 and you are confident the change is correct and complete. + +2. **When uncertain, always escalate to human review.** If you are unsure whether a concern is valid, whether the approach is sufficient, or whether you have enough context to judge, the verdict must be "Needs Human Review" — not LGTM. Your job is to surface concerns for human judgment, not to give approval when uncertain. A false LGTM is far worse than an unnecessary escalation. + +3. **Separate code correctness from approach completeness.** A change can be correct code that is an incomplete approach. If you believe the code is right for what it does but the approach is insufficient (e.g., treats symptoms without investigating root cause, silently masks errors that should be diagnosed, fixes one instance but not others), the verdict must reflect the gap — do not let "the code itself looks fine" collapse into LGTM. + +4. **Classify each ⚠️ and ❌ finding as merge-blocking or advisory.** Before writing your summary, decide for each finding: "Would I be comfortable if this merged as-is?" If any answer is "no," the verdict must be "Needs Changes." If any answer is "I'm not sure," the verdict must be "Needs Human Review." + +5. **Devil's advocate check before finalizing.** Re-read all your ⚠️ findings. For each one, ask: does this represent an unresolved concern about the approach, scope, or risk of masking deeper issues? If so, the verdict must reflect that tension. Do not default to optimism because the diff is small or the code is obviously correct at a syntactic level. + +--- + +## Holistic PR Assessment + +Before reviewing individual lines of code, evaluate the PR as a whole. Consider whether the change is justified, whether it takes the right approach, and whether it will be a net positive for the codebase. + +### Motivation & Justification + +- **Every PR must articulate what problem it solves and why.** Don't accept vague or absent motivation. Ask "What's the rationale?" and block progress until the contributor provides a clear answer. + > "I am not sure why is this needed. ... It's not immediately obvious whether this happens only for the bridge comparison tests or whether it can happen for real-life scenarios too." + +- **Challenge every addition with "Do we need this?"** New code, APIs, abstractions, and flags must justify their existence. If an addition can be avoided without sacrificing correctness or meaningful capability, it should be. + > "I don't think we should take this change, at all. A change which makes the VS runner see the same assets as the CLI runner, sure. But random extra hacking on the side, no." + +- **Demand real-world use cases and customer scenarios.** Hypothetical benefits are insufficient motivation for expanding API surface area or adding features. Require evidence that real users need this. + > "It is not clear to me whether you can hit a real-world scenario on 32-bit platforms where it makes a difference." + +### Evidence & Data + +- **Require measurable performance data before accepting optimization PRs.** Demand BenchmarkDotNet results or equivalent proof — never accept performance claims at face value. + > "Can you please share a benchmark using BenchmarkDotNet against public System.Text.Json APIs that demonstrates the improvement?" + +- **Distinguish real performance wins from micro-benchmark noise.** Trivial benchmarks with predictable inputs overstate gains from jump tables, branch elimination, and similar tricks. Require evidence from realistic, varied inputs. + > "Try to benchmark it with an input that varies randomly. Jump tables are great for trivial micro-benchmarks, but they are less great for real world code." + +- **Investigate and explain regressions before merging.** Even if a PR shows a net improvement, regressions in specific scenarios must be understood and explicitly addressed — not hand-waved. + > "Could you please inspect the regressions on why exactly it's an improvement there?" + +### Approach & Alternatives + +- **Check whether the PR solves the right problem at the right layer.** Look for whether it addresses root cause or applies a band-aid. Prefer fixing the actual source of an issue over adding workarounds to production code. + > "The offset behind `Flags.IndexMask` should always be correct. Instead of checking that the index is in range in all its usages, we should fix the root cause where the offset wasn't computed/updated correctly." + +- **When a PR takes a fundamentally wrong approach, redirect early.** Don't iterate on implementation details of a flawed design. Push back on the overall direction before the contributor invests more time. + > "I'm still hesitating whether separating FEATURE_HW_INTRINSICS from SIMD and MASKED_HW_INTRINSICS is the right approach ... An alternative would be to handle them like #113689 and fix the value numbering." + +- **Ask "Why not just X?" — always prefer the simplest solution.** When a PR uses a complex approach, challenge it with the simplest alternative that could work. The burden of proof is on the complex solution. + > "Wouldn't it be simpler to just do a regular mono stackwalk when we need to record and raise a sample?" + +### Cost-Benefit & Complexity + +- **Explicitly weigh whether the change is a net positive.** A performance trade-off that shifts costs around is not automatically beneficial. Demand clarity that the change is a win in the typical configuration, not just in a narrow scenario. + > "It is a performance trade-off. You will shift the costs around. It is not clear to me whether it would be a win at the end in the typical configuration." + +- **Reject overengineering — complexity is a first-class cost.** Unnecessary abstraction, extra indirections, and elaborate solutions for marginal gains are actively rejected. + > "This optimization smells funny. It seems overly complicated for little win. Is this path hot? Can we instead store the home directory?" + +- **Every addition creates a maintenance obligation.** Long-term maintenance cost outweighs short-term convenience. Code that is hard to maintain, increases surface area, or creates technical debt needs stronger justification. + > "The primary goal of this project is to minimize our long-term maintenance costs. Building multiple optimizing code generators would go against that goal." + +### Scope & Focus + +- **Require large or mixed PRs to be split into focused changes.** Each PR should address one concern. Mixed concerns make review harder and increase regression risk. + > "I think I'm going to break this into two pieces, even though that's more work." + +- **Defer tangential improvements to follow-up PRs.** Police scope creep by asking contributors to separate concerns. Even good ideas should wait if they're not part of the PR's core purpose. + > "Should probably be a separate PR." + +### Risk & Compatibility + +- **Flag breaking changes and require formal process.** Any behavioral change that could affect downstream consumers needs documentation, API review, and explicit approval — even when the change improves the codebase internally. + > "Introduce the new API in this PR. Remove the old check in another PR, mark it as breaking change and document it (as any other breaking change)." + +- **Assess regression risk proportional to the change's blast radius.** High-risk changes to stable code need proportionally higher value and more thorough validation. + > "I wanted to backport this change to .NET 10, potentially .NET 9, and wouldn't want to introduce any risky changes." + +### Codebase Fit & History + +- **Ensure new code matches existing patterns and conventions.** Deviations from established patterns create confusion and inconsistency. If a rename or restructuring is warranted, do it uniformly in a dedicated PR — not piecemeal. + > "This change is inconsistent with the rest of the global pointers. If we want to consider renaming these, I think we should do it in a separate PR and apply it consistently to all global pointers." + +- **Check whether a similar approach has been tried and rejected before.** If a prior attempt didn't work, require a clear explanation of what's different this time. + > "If it's not worthwhile, especially if it was previously tried and it wasn't obviously beneficial, then we should close the issue." + +--- + +## Correctness & Safety + +### Error Handling & Assertions + +- **Use `Debug.Assert` for internal invariants, not exceptions.** For internal-only callers, assert assumptions rather than throwing `ArgumentException`. Prefer `Debug.Assert(value != null)` over the null-forgiving operator (`!`). + > "Since there are no public callers, this should be an assert, not an ArgumentException." — bartonjs + +- **Use `throw` for reachable error paths, `UnreachableException` for exhaustive switches.** When a code path might be hit at runtime, throw an exception rather than asserting. Use `throw new UnreachableException()` for default cases in exhaustive switches. Use `PlatformNotSupportedException` (not `NotSupportedException`) for platform gaps. In native code, use `_ASSERTE(!"message")`. + > "We prefer throw rather than asserts so it is more apparent if some scenario makes it here." — VSadov + +- **Include actionable details in exception messages.** Use `nameof` for parameter names. Include the unsupported type or unexpected value. Never throw empty exceptions. + > "You should add some message here: `throw new ArgumentException($\"Unknown ArrayFunctionType: {functionType}.\", nameof(method));`" — jkoritzinsky + +- **Initialize output parameters in all code paths.** When a method has `out` parameters or pointer outputs (`bytesWritten`, `numLocals`), ensure they are initialized to a defined value in all error paths. + > "Clear numLocals here (or at start of the method) so that it is initialized in all error cases?" — jkotas + +- **Handle OOM with exceptions or fail-fast, never asserts.** Use `ThrowOutOfMemory` or `EEPOLICY_HANDLE_FATAL_ERROR`, not asserts. In interpreter loops, use `nothrow new` and check for null. + > "OutOfMemory handling should not be done by asserts. It should throw exception; or if exception is not viable, fail with fail fast." — jkotas + +- **Use `ThrowIf` helpers over manual checks.** Use `ArgumentOutOfRangeException.ThrowIfNegative`, `ObjectDisposedException.ThrowIf`, etc. instead of manual if-then-throw patterns. + > "This if condition should not be necessary, as it's going to be checked by ThrowIfNegative." — stephentoub + +- **Challenge exception swallowing that masks unexpected errors.** When a PR adds try/catch blocks that silently discard exceptions (`catch { continue; }`, `catch { return null; }`), question whether the exception represents a truly expected, recoverable condition or an unexpected error signaling a deeper problem (race conditions, memory corruption, build environment issues). Silently catching exceptions that "shouldn't happen" hides root causes and makes debugging harder. The default disposition should be to let unexpected exceptions propagate or fail fast so the real issue gets investigated. + > "Why do we want to mask this error? ... Our general strategy in AOT compilers is to fail the compilation when the input is malformed. It is expected that the malformed input can and will cause the compiler to crash or fail." — jkotas + +### Thread Safety + +- **Use `Volatile` or `Interlocked` for cross-thread field access.** Fields written on one thread and read on another must use `Volatile`, `Volatile.Read/Write`, or `Interlocked`. The `??=` operator is not thread-safe. `Nullable` is not safe for caching (two-field struct tears). Do not use shared mutable arrays without synchronization. + > "field ??= is not thread-safe." — jkotas; "Nullable\ is a struct with two fields. This pattern has a race condition caused by tearing." — jkotas + +- **Use `TickCount64` for timeout calculations.** Use `Environment.TickCount64` (long) instead of `Environment.TickCount` (int) to avoid integer overflow. + > "Should this use long and `Environment.TickCount64` to avoid integer overflow issues?" — jkotas + +### Security + +- **Guard integer arithmetic against overflow.** Guard size computations involving multiplication (e.g., `newCapacity * sizeof(T)`) against integer overflow. Use patterns correct by construction. + > "We have a lot of scars from integer overflow security bugs... This change is switching from code that follows best practices to a potentially vulnerable pattern." — jkotas + +- **Clean sensitive cryptographic data after use.** Always clear key material with `CryptographicOperations.ZeroMemory`. When using `PinAndClear` but copying to another buffer, clear the original too. Use non-short-circuit operators (`|`) in verification code to prevent timing leaks. + > "This is using PinAndClear to avoid the GC making a copy... but it's itself making a copy and not clearing the original." — bartonjs + +- **Don't proactively send credentials without opt-in.** Never send authentication credentials (especially Basic auth) before receiving a challenge. + > "This is problematic and we will have difficulty with security group as it especially with basic AUTH leaks the credentials." — wfurt + +- **Limit `stackalloc` to ~1KB and validate size.** Don't stackalloc based on user-controlled or large input sizes. Move stackalloc to just before usage, not before early returns. + > "We typically limit stackallocs to ~1K." — stephentoub + +### Correctness Patterns + +- **Fix root cause, not symptoms or workarounds.** Investigate and fix the root cause rather than adding workarounds or suppressing warnings. Revert broken commits before layering fixes. + > "Let's try to investigate the root cause instead of taking this fix as-is since there could be other issues/AVs related to the mangling of the list." — jkotas + +- **Prefer safe code over unsafe micro-optimizations.** Do not introduce `Unsafe.As`, `Unsafe.AsRef`, or raw pointers without demonstrable performance need. Prefer Span-based APIs. If performance is the issue, prefer fixing the JIT. + > "I do not want to introduce unsafe code for things like this. If it's material, the cast should be elided by the JIT." — stephentoub + +- **Use `Unsafe.BitCast` for same-size type punning.** Prefer `Unsafe.BitCast` over `Unsafe.As` for type punning between value types of the same size. + > "Unsafe.BitCast is more correct (avoids undeclared misaligned access) and less dangerous than Unsafe.As here." — jkotas + +- **Delete dead code and unnecessary wrappers.** Remove dead code, unnecessary wrappers, obsolete fields, and unused variables when encountered or when the only caller changes. + > "Unnecessary wrapper", "Dead code that I happen to notice", "This is the only use of m_canBeRuntimeImpl. It can be deleted." — jkotas + +- **Handle `SafeHandle.IsInvalid` before `Dispose`.** Check `IsInvalid` (not null) on returned SafeHandles. Get the exception before calling `Dispose`, since Dispose might clear the error state. + > "`if (handle.IsInvalid) { Exception ex = Interop.Crypto.CreateOpenSslCryptographicException(); handle.Dispose(); throw ex; }`" — vcsjones + +- **Seal classes when `Equals` uses exact type matching.** If a class implements `Equals` with `GetType()` comparison, seal the class to prevent subtle inheritance bugs. + > "Is there a reason why this Equals implementation is exact type match only even though ContextHolder isn't sealed? I'd prefer to block this class of failure by sealing the class." — kg + +- **Use `Environment.ProcessPath` and `AppContext.BaseDirectory`.** Use these instead of `Process.GetCurrentProcess().MainModule?.FileName` and `Assembly.Location` for NativeAOT/single-file compatibility. + > "Process.GetCurrentProcess().MainModule?.FileName should be same as Environment.ProcessPath." — jkotas + +- **File name casing must match csproj references exactly.** Linux is case-sensitive. New source files must be listed in the `.csproj` if other files in that folder are explicitly listed. + > "The build is failing because Linux is a case-sensitive file system and your csproj file and the file name differ by case." — vcsjones + +- **Prefer correct-by-construction designs.** Prefer designs that are correct by construction (e.g., scanning IL) over manually maintained parallel data structures. A missed optimization is better than silent bad codegen. + > "I'd go with the correct-by-construction approach if it's an option." — MichalStrehovsky + +- **Allocate on the correct loader allocator for collectibility.** When allocating runtime data structures for generic instantiations, use the correct loader allocator accounting for collectibility of type arguments. + > "Consider MethodInNonCollectibleAssembly\(): This method instantiation should be allocated on collectible loader allocator. As written, it will have a memory leak." — jkotas + +- **Backport targeted fixes, not refactorings.** When backporting to servicing branches, create small targeted fixes. Backporting large refactorings introduces unnecessary risk. + > "If we need the fix in .NET 10 for Android, we should do a small targeted fix that just adds the few lines under Android ifdef." — jkotas + +### JIT-Specific Correctness + +- **JIT lowering must not double-lower nodes.** Never call `LowerNode` on an already-lowered node. Return newly created nodes for the caller to lower. Constant folding belongs in import/morph, not lowering. + > "Lower is not supposed to be called twice on the same node generally." — EgorBo + +- **Mark collectible ALC test methods `NoInlining`.** Methods that touch collectible assembly load contexts must be `[MethodImpl(MethodImplOptions.NoInlining)]` to prevent the JIT from keeping references alive. + > "This needs to be marked as no-inlining too. It is valid for JIT to inline this method and leave a reference to collectible assembly load context in a local." — jkotas + +--- + +## Performance & Allocations + +### Measurement & Evidence + +- **Performance changes require benchmark evidence.** Include BenchmarkDotNet or EgorBot numbers before merging. Validate with real-world scenarios, not just microbenchmarks. + > "Performance related changes without numbers have high probability of being performance regressions in practice." — jkotas + +- **Justify binary size increases with real-world measurements.** Changes that increase binary size require measured wall-clock improvements on real-world apps, not just instruction counts. + > "I would like to see number for a Blazor app (total size / bytes saved by this change)." — jkotas + +- **Avoid premature optimization with object pools and caches.** Do not introduce global caches or object pools without evidence they are needed. Prefer making the underlying operation faster. + > "This pool looks like a premature optimization." — jkotas + +### Allocation Avoidance + +- **Avoid closures and allocations in hot paths.** When a lambda captures locals creating a closure, consider using a static delegate with a state parameter (value tuple). Avoid string concatenation; use span-based operations. + > "Since this is capturing `data` and `context` in the closure it's allocating a closure for every call. The usual fix is to make the callback take a TState, and just pass through a value-tuple." — bartonjs + +- **Pre-allocate collections when size is known.** Pass capacity to `Dictionary`, `HashSet`, `List` constructors when the expected count is available. + > "We can pre-allocate the dictionary of the right size." — jkotas + +- **Structs in dictionaries need `IEquatable` and `GetHashCode`.** Without these, the runtime falls back to boxing allocations for equality comparison. + > "If a struct doesn't override equality comparison logic, the runtime will end up using a fallback that boxes the value." — MihaZupan + +- **Avoid Pinned Object Heap for non-permanent objects.** POH is never compacted and effectively gen2. Only use for objects surviving as long as the process. + > "We avoid the POH for objects that may have a shorter lifespan, typically only using it for objects that will survive as long as the process does." — stephentoub + +- **Suppress `ExecutionContext` flow for infrastructure timers.** When allocating `Timer` or similar background infrastructure, suppress EC flow to avoid capturing unrelated `AsyncLocal`s that leak memory. + > "We'll want to suppress the ExecutionContext during the timer allocation to avoid capturing unrelated asynclocals." — MihaZupan + +### Code Structure for Performance + +- **Place cheap checks before expensive operations.** Order conditionals so cheapest/most-common checks come first. Move expensive work after early-exit checks. + > "These checks are cached cheap bit tests. We should do them first, and run the more expensive IL header decoder only when the modes do not match." — jkotas + +- **Allocate resources lazily where possible.** Allocate expensive resources on first use, not during initialization. Avoid forcing type initialization during startup. + > "We try to do things lazily where possible since it is good for performance." — jkotas; "Do not force initialization during runtime startup just to make cDAC work. Startup is our number one perf problem." — jkotas + +- **Extract throw helpers into `[DoesNotReturn]` methods.** Move throwing logic from error paths into separate static local functions or helper methods to allow the JIT to inline the success path. + > "Please move the body of this `if (throwOnFailure)` block to a separate [DoesNotReturn] throwing static local function." — stephentoub + +- **Avoid O(n²) patterns in collections and hot paths.** Watch for linear scans inside loops, repeated `RemoveAt` in loops. Use `RemoveAll`, single-pass restructuring, or appropriate data structures. + > "Since RemoveParsedValue performs a linear scan, this change makes the setter have a quadratic worst-case complexity." — MihaZupan + +- **Cache repeated accessor calls in locals.** Store the result of repeated property/getter calls in a local variable. + > "Maybe read it out once into local to reduce number of calls to m_type_data_get_type?" — lateralusX + +- **Separate hot data from rarely-used data in runtime structures.** Keep frequently accessed data inline; move rarely-used data (GCInfo, DebugInfo) to separate structures. + > "The code header structure is intentionally designed to separate hot and rarely used data." — jkotas + +- **Compute constant data at compile time, not execution time.** In interpreter and similar hot paths, pre-compute metadata lookups and type checks during the compilation phase. + > "This computation is constant and should be done at compile time." — BrzVlad + +- **Consider scalability, not just throughput.** Evaluate whether data structures, caches, and locking strategies will hold up at high cardinality or under concurrent load. Watch for unbounded collection growth, lock contention that worsens with core count, and O(1) assumptions that break at scale. + +### Specific API Choices + +- **Use `AppContext.TryGetSwitch` with a static readonly property.** Cache AppContext switches in `static bool Prop { get; } = AppContext.TryGetSwitch(...)` so the JIT can dead-code-eliminate unreachable paths. + > "`private static bool SwitchEnabled { get; } = AppContext.TryGetSwitch(..., out bool enabled) && enabled;` This makes it readonly and lets the JIT delete the unreachable code paths." — MihaZupan + +- **Do not cache `typeof` expressions in .NET Core.** `typeof(...)` is JITed into a constant; caching it is a de-optimization. Similarly, don't store `ArrayPool.Shared` in variables—it breaks devirtualization. + > "Caching typeof(...) is de-optimization in .NET Core. typeof(...) is JITed into a constant." — jkotas + +- **Use `CollectionsMarshal` for large value-type dictionary lookups.** Use `GetValueRefOrAddDefault` or `GetValueRefOrNullRef` to avoid copying large structs. Use `ValueListBuilder` on hot paths. + > "You can use CollectionsMarshal.GetValueRefOrAddDefault here... Avoids copy of the large EventMetadata struct." — jkotas + +- **Use `sizeof` instead of `Marshal.SizeOf` for blittable structs.** `sizeof` is more correct and significantly faster when no marshalling is involved. + > "It is more correct and a lot faster to use sizeof instead of Marshal.SizeOf." — jkotas + +- **Use the idiomatic `(uint)index >= (uint)length` bounds check.** The JIT recognizes this pattern and optimizes it. Slice spans before iterating to avoid per-element bounds checks. + > "The JIT recognizes the idiomatic pattern and optimizes it down where it is safe already." — tannergooding + +- **Source generators must be properly incremental.** Do not store Roslyn symbols (`ISymbol`, `Compilation`) in incremental pipeline steps. Output must be deterministic with Ordinal-sorted lists. + > "Make your generators properly incremental or don't ship them at all, because the alternative is that you'll murder the IDE." — Sergio0694 + +- **Avoid LINQ and records in low-level compiler codebases.** In CG2/ILC and AOT tools, use direct loops instead of LINQ and readonly structs instead of records. Use concrete types over interfaces in private code. + > "I'd avoid `using System.Linq` to steer clear of perf traps." — MichalStrehovsky + +- **Use `ValueListBuilder` for dynamic array building in BCL.** Use `ValueListBuilder` (with pooling) or `ArrayBuilder`. Use stackalloc for small sizes, array pool when too large. + > "ValueListBuilder is the centralized type for building arrays in BCL." — huoyaoyuan + +--- + +## API Design & Contracts + +- **New public APIs require approved proposals before PR submission.** All new API surface must go through API review. PRs adding unapproved APIs will be closed. The implementation must match exactly what was approved. + > "We do not accept PRs for unapproved APIs." — jkotas + +- **Use `internal` for new APIs pending API review.** If the API is needed immediately for implementation, mark it `internal` and file a review request separately. + > "This needs to go through API Review for it to be public. You should use internal for now." — jozkee + +- **Parameter names must match between ref and src.** Renaming a public API parameter (including case changes) is a breaking change affecting named arguments and late-bound scenarios. + > "The approved API calls the second parameter value, not result. The mismatch is also why the build failed." — vcsjones + +- **Align exception types and validation order across platforms.** Validate arguments first (`ArgumentNullException`, then `ArgumentException`), then `PNSE`, then `ObjectDisposedException`, then perform the operation. Throw the same exception types on all platforms. + > "My exception order is: 1. ArgumentExceptions (null first, then logical) 2. PNSE 3. ObjectDisposedException 4. 'Do the thing' exceptions." — vcsjones + +- **`Try` APIs should return `false` only for the common expected failure.** Throw for everything else (corruption, permissions, invalid arguments). Try methods must always throw on invalid arguments. + > "The usual contract for Try... API is to return false for the specific (most common) reason only and throw for everything else." — jkotas + +- **Don't expose mutable options after construction.** If values are captured at construction time, don't expose a mutable options object. Don't reference private field names or internal types in user-facing error messages. + > "Exposing a mutable ZLibCompressionOptions object after construction could be misleading." — iremyux + +- **Use `PlatformNotSupportedException` for platform limitations.** When an operation can't complete in the current environment but could on a different platform, throw PNSE. Don't impose artificial limits beyond OS capabilities. + > "Our general policy for System.IO is to surface underlying system limitation without imposing additional artificial restrictions." — jkotas + +- **.NET APIs should compensate for platform quirks.** Public APIs should work consistently across platforms. When adding overloads, check F# compatibility for implicit conversion ambiguities. + > "The added value of .NET as a platform is that we compensate for quirks of the underlying platforms and we make things just work." — jkotas + +- **Follow the obsoletion process for deprecated APIs.** Pick the next available SYSLIB diagnostic ID, add `[Obsolete]`, and use `[EditorBrowsable(Never)]` with `[OverloadResolutionPriority(-1)]` for overload fixes. + > "We have an obsoletion process in place which involves picking the next available SYSLIB id." — eiriktsarpalis + +- **New GC-EE interface methods must be appended last.** Always add new methods as the last method on the interface to preserve vtable slot ordering. + > "This needs to be the last method on the interface to avoid changing vtable slots for existing methods." — jkotas + +- **New virtual methods must work with unoverridden derived types.** The default implementation must behave identically to calling the pre-existing equivalent APIs. + > "We have to expect that the new method will be used with derived writers that won't yet have overridden it." — stephentoub + +- **Avoid unsigned types for lengths in public APIs.** Prefer `int` or `long` for length parameters. Use named types instead of `ValueTuple` across file boundaries. + > "We generally don't use unsigned types for lengths in public APIs." — rzikm + +- **Start core component changes with an issue.** Changes to host, VM, or JIT should start with a GitHub issue describing the problem and motivation before submitting a PR. + > "This sort of change really should have started in an issue as it involves changing a core component and there is a lot to consider." — AaronRobinsonMSFT + +--- + +## Code Style & Formatting + +- **Use well-named constants instead of magic numbers.** No raw hex or decimal constants without explanation. Don't duplicate magic constants across files. + > "0x7F00 says nothing to the typical reader. Adding a comment that explains it means `(float)Int128.MaxValue`... then means a lot." — tannergooding + +- **Use `var` only when the type is obvious from context.** Use explicit types for casts, method returns, and async infrastructure. Never use `var` for numeric types. + > "These seem like unacceptable uses of var." — bartonjs + +- **Use PascalCase for constants; descriptive names for booleans.** All constant locals and fields use PascalCase (except interop constants matching external names). Boolean fields should be positive and descriptive (`_hasCurrent` not `valid`). + > "We use PascalCasing to name all our constant local variables and fields." — bartonjs + +- **Name methods to accurately reflect their behavior.** Update names when behavior changes. `Get*` implies a return value; use `Print*/Display*` for void. `ThrowIf` not `ThrowExceptionIf`. + > "This method is not returning anything after this change, so Get... name does not fit." — jkotas + +- **Prefer early return to reduce nesting.** Use early returns for short/error cases to avoid unnecessary nesting. Put the error case first, success return last. + > "Rather than having the extra layer of indentation for the else block, could we do it as: `if (...) { return ...; }`" — stephentoub + +- **Avoid `using static` and `#region` in new code.** `using static` is costly when reading code outside IDEs (e.g., GitHub review). `#region` gets out of date quickly. + > "`using static` adds an astronomical cost when an IDE isn't available." — bartonjs + +- **Place local functions at method end, fields first in types.** Local functions go at the end of the containing method. Fields are the first members declared in a type. + > "The generally agreed upon pattern has been that local functions are placed at the end of the method." — AaronRobinsonMSFT + +- **Narrow warning suppression to smallest scope.** Avoid file-wide `#pragma` suppressions. Disable only around the specific line that triggers the warning. + > "Suppressing warnings broadly is generally bad practice." — AaronRobinsonMSFT + +- **Use pattern matching and `is`/`or`/`and` patterns.** Prefer `is` patterns and C# pattern matching over manual type checks and comparisons. Use named parameters for boolean arguments. + > "`return !(typeDesc.Category is TypeFlags.Boolean or TypeFlags.Char);`" — jkotas + +- **Do not initialize fields to default values (CA1805).** The CLR zero-initializes fields. Explicit `= false`, `= 0`, `= null` is redundant. + > "CA1805: Do not initialize unnecessarily." — MichalStrehovsky + +- **Sealed classes do not need the full Dispose pattern.** A simple `Dispose()` is sufficient since no derived class can introduce a finalizer. + > "Given that the class is marked sealed now, I personally don't think the full dispose pattern is needed." — Youssef1313 + +- **Prefer table-driven approaches over excessive case statements.** For hardware intrinsics and pattern-heavy code, use lookup tables (`AuxiliaryJitType`, `SpecialCodeGen` flags) instead of many explicit case entries. + > "I think it'd be better to use the AuxiliaryJitType and mark these as SpecialCodeGen than to add a bunch of extra table entries." — tannergooding + +- **Order struct fields to minimize padding.** In C/C++ struct definitions, order fields by size (pointers first) to reduce padding. + > "Maybe put pointers first to prevent padding." — lateralusX + +--- + +## Consistency with Codebase Patterns + +### PR Hygiene + +- **Keep PRs focused on their stated scope.** No accidental file modifications, no unrelated refactoring, no whitespace noise, no build artifacts. Each PR should serve a single purpose. + > "Please be more deliberate about your pull requests... it makes for a very muddy source history." — bartonjs + +- **Do large refactorings and renames in separate PRs.** Separate no-diff refactors from functional changes. Mechanical renames should be separate from logic changes. + > "I always prefer to do the no-diff refactors first and build the diff changes on top." — AndyAyersMS + +- **Merge to main first, then backport to release branches.** Use the `/backport` command. Backports to servicing are limited to security bugs, regressions, and reliability issues. + > "In general, performance related fixes do not meet the bar, unless they are fixing significant regression." — jkotas + +### Code Reuse & Deduplication + +- **Extract duplicated logic into shared helper methods.** Fix improvements inside shared helpers so all callers benefit. + > "Would it be better to move this to a helper method instead of duplicating it?" — tarekgh + +- **Move shared code to shared files, not duplicated across runtimes.** When identical code exists across CoreCLR and NativeAOT, move it to the shared partition (using `#if !MONO` if needed). + > "There is quite a bit of identical code between NativeAOT and CoreCLR. Can we move it into the shared file?" — jkotas + +- **Use existing APIs instead of creating parallel ones.** Before introducing new types, enums, or helpers, check if existing ones serve the same purpose. Fix existing utilities rather than introducing duplicates. + > "Can you use the existing SignatureAttributes.Instance instead? It means the same thing." — jkotas + +- **Delete dead code and unused declarations aggressively.** When removing code, also remove helper methods, enum values, function declarations, and resx strings that are no longer used. + > "This function isn't used. Please delete." — davidwrighton + +### Established Conventions + +- **Store error strings in `.resx`, not inline code.** Reference via the `SR` class. When removing code that uses a resx string, delete the unused string entry. + > "We do not store string message in code. Instead, they should be stored in .resx, with optional format argument and referenced with SR." — huoyaoyuan + +- **Sort lists and entries alphabetically.** Lists of areas, configuration entries, resx entries, entrypoint/export lists, and ref source members should be maintained in alphabetical order. + > "The list of areas looks sorted in alphabetical order." — jkotas + +- **Don't modify auto-generated files or `eng/common` manually.** Change the generator or source definition instead. Files in `eng/common` are synced from dotnet/arcade. + > "Things in eng/common come from the dotnet/arcade repository. Your fixes here will be undone the next time arcade is synced." — vcsjones + +- **Use `DOTNET_` prefix for environment variables, not `COMPlus_`.** New runtime environment variables must use `DOTNET_` exclusively. + > "The COMPlus names are legacy that we'd like to phase out. I don't think we should add support for them in new features." — agocke + +- **Match existing style in modified files.** The existing style in a file takes precedence over general guidelines. Do not change existing code for style alone. + > "If a file happens to differ in style from these guidelines, the existing style in that file takes precedence." — huoyaoyuan + +- **Prefer `sizeof` over `Unsafe.SizeOf` consistently.** A pass was done to replace all `Unsafe.SizeOf` uses. Do not reintroduce them. + > "We have done a pass to delete all Unsafe.SizeOf uses and replaced them with sizeof." — jkotas + +### Runtime-Specific Patterns + +- **Consider NativeAOT parity for runtime changes.** When changing CoreCLR behavior, verify whether the same change is needed for NativeAOT. + > "The code you have changed is not used on NativeAOT. Do we need the same change for NativeAOT as well?" — jkotas + +- **Keep interpreter behavior consistent with the regular JIT.** Follow the same patterns, naming, error codes (`CORJIT_BADCODE`), and macros (`NO_WAY`). Use `FEATURE_INTERPRETER` guards. + > "Should we call it NO_WAY like in a regular JIT? I think the more similar the interpreter JIT to the regular JIT, the better." — jkotas + +- **Source generators: no file locks, diagnostics from analyzers only.** Generators should bypass invalid state gracefully. A separate analyzer should produce diagnostics. + > "A generator should never lock files on disk." — jkoritzinsky + +- **Ref assembly conventions.** No `using` directives (fully qualify types), empty method bodies or `throw null`, genapi-style formatting, alphabetical member order. TFM-specific APIs go in separate files. + > "Generally the ref source does not have usings." — vcsjones + +--- + +## Testing + +- **Always add regression tests for bug fixes and behavior changes.** Prefer adding `[InlineData]` test cases to existing test files rather than creating new ones. Ensure new test files are included in the csproj. + > "The PR needs a regression test added. TypeInfoTests.cs is a good place to add it (add new InlineData)." — jkotas + +- **Use platform-specific test attributes correctly.** Use `[PlatformSpecific]`, `[ConditionalFact]`, or `[ActiveIssue]` for skip logic rather than runtime if-checks. `ConditionalFact` is required for `SkipTestException` to work. + > "This needs to be conditional fact for throw new SkipTestException inside the test." — jkotas + +- **Test edge cases, error paths, and all affected types.** Include empty strings, negative values, boundary conditions, Turkish 'i', surrogate pairs. Test both true and false for boolean options. Choose inputs that can't accidentally pass if output wasn't touched. + > "Pick an input that doesn't decode to all 0s so that the test can't pass even if the output wasn't touched at all." — MihaZupan + +- **Test assertions must be specific.** Assert exact expected values (exact `OperationStatus`, exact byte counts), not broad conditions. Ensure tests actually fail when the fix is reverted. + > "The current asserts are too broad to be useful." — MihaZupan + +- **Delete flaky and low-value tests rather than patching them.** Do not add tests known to be flaky. If a test relies on fragile runtime details and cannot be made reliable, prefer deletion. + > "It would be better to delete the test. No point in adding flaky tests." — jkotas + +- **Make test data deterministic and culture-independent.** Create `CultureInfo` with explicit format settings. Use `[Theory]` with `[InlineData]` over individual `[Fact]` methods. + > "I would suggest in the test you create a culture like `var culture = new CultureInfo(\"de-DE\"); culture.DateTimeFormat.AbbreviatedMonthGenitiveNames = [...]`" — tarekgh + +- **Use `PLACEHOLDER` for test passwords.** Avoids false positives from credential scanning tools. + > "Sometimes credscan gets tetchy about tests when we don't want it to. Their recommendation is to use the value PLACEHOLDER." — bartonjs + +- **Use checked builds for CI, lower priority for regression tests.** Use checked (not debug) CoreCLR builds for CI. New JIT regression tests should typically be `CLRTestPriority 1`. + > "Debug build of CoreCLR is very slow. We typically use checked build for testing." — jkotas + +- **Use `RemoteExecutor` for tests with process-wide shared state.** Tests that modify shared state should use `RemoteExecutor` for isolation. Avoid hardcoded paths; use temp files. Do not add heavy dependencies like `Microsoft.CodeAnalysis.CSharp` to test assemblies. + > "Avoid dependency on Microsoft.CodeAnalysis.CSharp in these tests. It would prevent this whole test assembly from being able to run on devices, wasm and nativeaot." — jkotas + +- **Catch only expected exceptions in fuzz tests.** Catching all exceptions masks bugs like undocumented exceptions escaping the API. + > "Would it be possible to catch only the exceptions that we expect here?... It has helped me to find that the library was throwing undocumented exceptions." — adamsitnik + +- **Use modern xUnit patterns for xUnit-based tests.** In xUnit test projects (for example, most libraries tests), use `Assert.*` instead of the legacy `return 100 == success` pattern, use `[Fact]`/`[Theory]`, prefer `ThrowsAnyAsync` for cancellation, and name regression test classes after the issue number (e.g., `Runtime_117605`). Legacy non-xUnit tests under `src/tests` may continue to use the existing `return 100` convention. + > "Can we change the tests here to use Asserts and not the legacy 'return 100 == success' model?" — jkoritzinsky + +- **Reduce test output volume.** Avoid megabytes of console output. Use `Thread.Sleep` with fewer iterations instead of busy loops. + > "This will produce megabytes of output. Can we do something less visible, like Thread.Sleep(10) and change the for-loop to go till like 200?" — jkotas + +- **Follow naming conventions for regression test directories.** In `src/tests/Regressions/coreclr/`, use `GitHub_` for the directory and `test` for the test name. + > "Please follow the pre-existing pattern for naming. The directory name should be GitHub_122933 and the test name should be test122933." — jkotas + +--- + +## Documentation & Comments + +- **Comments should explain why, not restate code.** Delete comments like `// Get the types` that just duplicate the code in English. Don't include historical context about why code changed. + > "Comments that just duplicate the code in plain English are not very useful. This comment should explain why we are doing this." — jkotas + +- **Delete or update obsolete comments when code changes.** Stale comments describing old behavior are worse than no comments. + > "The whole comment starting with `Note:` can be deleted. It is no longer applicable." — jkotas + +- **Track deferred work with GitHub issues and searchable TODOs.** Reference a tracking issue in TODO comments with a consistent prefix (e.g., `TODO-Async:`). Remove ancient TODOs that will never be addressed. + > "Could you please tag all these places that need review with async TODO so that they can be found easily and none of them falls through the cracks?" — jkotas + +- **Don't duplicate comments on interface implementations.** Documentation comments belong on the interface definition. Duplicating leads to divergence. + > "It is enough to have these comments on the interface. Duplicating them is just going to lead to the comments diverging over time." — jkotas + +- **Add XML doc comments on all new public APIs.** These seed the official API documentation on learn.microsoft.com. Properties should start with "Gets the ..." or "Gets or sets the ...". Do not add XML docs to test code. + > "Please also include /// comments on all the new APIs (they seed the api docs)." — MihaZupan + +- **Use SHA-specific or commit-based links in documentation.** Don't use branch-relative links that break when files move. + > "Best to use sha-specific links." — richlander + +- **Reference ECMA-335 and spec sources in metadata code.** When parsing signatures and metadata, cite the relevant ECMA-335 section. Cite CAVP/ACVP sources in crypto test vectors. + > "Perhaps reference the signature format from ECMA-335 we are following here." — AaronRobinsonMSFT + +- **File breaking change documentation for behavioral changes.** Open an issue in dotnet/docs using the template, send notification to the .NET Breaking Change Notification DL. Applies even to prerelease-to-prerelease changes. + > "We just need to open an issue describing the break using the breaking change template." — tannergooding + +- **Use established terminology in user-facing text.** Do not expose internal type names, private field names, or codenames like "Roslyn" in public docs or error messages. + > "'non-explicit type' is not an established term." — jkotas; "Roslyn is our internal codename. It should not be used in public docs." — jkotas + +- **Retain copyright headers and license information.** All C# and C++ source files must include the standard license header, including test files. When porting from other projects, retain original copyright and update THIRD-PARTY-NOTICES.TXT. + > "All C# and C++ source files should have license header (incl tests)." — jkotas + +--- + +## Platform & Cross-Platform + +- **Use `BinaryPrimitives` for endianness-safe reads.** Use `ReadInt32LittleEndian`/`BigEndian` rather than pointer casts. Separate endianness-specific reads from target-endianness reads. + > "If we're reading this as a 64-bit value... On big endian the result is OverflowException." — tmds + +- **Use cross-platform vector APIs over ISA-specific intrinsics.** Prefer `Vector128/256/512.IsHardwareAccelerated` and cross-platform APIs (`.Shuffle`, `.Min`) over `Avx512BW`, `SSE2`. Use `BitOperations` for portable bit manipulation. + > "Would you want to update this to use the xplat APIs instead? Swap Avx512BW.IsSupported -> Vector512.IsHardwareAccelerated." — tannergooding + +- **Use correct platform/feature defines.** Use `TARGET_*`/`HOST_*` defines rather than compiler-provided defines (`__wasm__`). Use `HOST_*` for build machine code, `TARGET_*` for target platform. Use `PORTABILITY_ASSERT` for unimplemented platform code. + > "The combinations of styles e.g. HOST_WINDOWS and __wasm__ next to each other does not look good." — jkotas + +--- + +## Native Code & Interop + +### C++ Style + +- **Don't use `auto` in the runtime C++ codebase.** Use explicit types. Exception: unspeakable types like lambdas. + > "We don't use auto in the runtime code base." — AaronRobinsonMSFT + +- **Use `nullptr`, `void*`, and native C++ types over legacy aliases.** Prefer `nullptr` over `NULL`, `void*` over `LPVOID`. Use `WCHAR` (not `wchar_t`) in Windows host code. Use `.inc` suffix for multiply-included files. + > "We prefer `nullptr` in large chunks of new code." — jkotas; "LPVOID is Windows SDK alias for void* with legacy baggage." — jkotas + +- **Match `#endif` comments to `#ifdef` exactly.** Add comments on `#else`/`#endif` for non-trivial blocks. Consistent brace placement and four-space indentation. + > "The common style in CoreCLR is to match the `#ifdef` exactly even if there is `#else`." — jkotas + +- **Prefer `static_cast` over C-style casts.** C-style casts are more permissive than needed and can silently degrade to `reinterpret_cast`. + > "`static_cast<>` is about enforcing the narrowest contract we can afford... a C style cast is a lurking `reinterpret_cast<>`." — AaronRobinsonMSFT + +### Runtime & VM Patterns + +- **Use correct VM contracts and QCall patterns.** QCalls that may throw need `BEGIN_QCALL`/`END_QCALL`. Simple QCalls use `QCALL_CONTRACT_NO_GC_TRANSITION`. All VM methods need `STANDARD_VM_CONTRACT` or `WRAPPER_NO_CONTRACT`. + > "QCalls need BEGIN_QCALL and END_QCALL if exceptions will be thrown." — AaronRobinsonMSFT + +- **Keep GC protection correct around managed references.** Ensure all GC references are `GCPROTECT`-ed before GC-triggering calls. After GC-triggering calls, use `ObjectFromHandle(handle)` for a fresh reference. + > "MethodDescCallSite is GC triggers, so you need to protect all GC references when it is called." — jkotas + +- **Avoid dynamic allocation on fatal error paths.** Use stack-allocated buffers. Use simple synchronization (Interlocked with spin-wait) instead of Monitor/lock. + > "Given that this is a fatal error path that can be reached due to OOM, I would prefer not allocating memory here." — janvorli + +- **Avoid thread-local objects with destructors in CoreCLR.** Destruction order is arbitrary. Tie lifetime to the CoreCLR Thread object. Prefer `PLATFORM_THREAD_LOCAL` from minipal over C++ `thread_local` in perf-critical paths. + > "Having a thread local object with a destructor is a recipe for possible problems due to the fact that the order of their destruction is arbitrary." — janvorli + +- **Use `SET_UNALIGNED` macros for potentially unaligned writes.** In code generation stubs, use `SET_UNALIGNED_32/64` rather than direct pointer dereferencing. + > "This should use SET_UNALIGNED_64." — jkotas + +- **Zero-initialize arrays and buffers that may be partially used.** Zero-init allocated arrays whose elements have destructors. Zero-init EH tables, C arrays, and similar structures. + > "Should the content of the array be zero-initialized? Otherwise, the destructor may access uninitialized memory if there is an exception thrown mid-flight." — jkotas + +- **Add static asserts for hardcoded structural offsets.** When using hardcoded offsets to access struct fields (especially in assembly), add static asserts to verify them. + > "It would be good to add some static asserts to verify that these offsets are valid. I am worried that some future change could break these." — janvorli + +- **Use minipal for new platform abstractions.** Use minipal (new) instead of PAL (legacy) for platform abstraction in new CoreCLR code. Use `ALTERNATE_ENTRY` (not `LOCAL_LABEL`) for assembly labels called from outside their function. + > "The minipal is the new place for abstracting platform dependencies." — janvorli + +- **Use `JITDUMP` and `LOG` macros, not `printf`.** In JIT code use `JITDUMP`. In CoreCLR VM use `LOG()`/`LOGGING` defines. Do not use `printf` or `Console.WriteLine` in production native code. + > "This should probably be using the JITDUMP or alternative API rather than just calling `printf`." — tannergooding + +### P/Invoke & Marshalling + +- **Prefer 4-byte `BOOL` for native interop marshalling.** Use `UnmanagedType.Bool`. Verify P/Invoke return types match native signatures exactly—mismatches may work on 64-bit but fail on 32-bit/WASM. + > "bool marshalling has always been bug prone area. The 4-byte bool (UnmanagedType.Bool) tends to be the least bug-prone option." — jkotas diff --git a/.github/skills/vmr-codeflow-status/SKILL.md b/.github/skills/vmr-codeflow-status/SKILL.md new file mode 100644 index 000000000000..01e3ce999301 --- /dev/null +++ b/.github/skills/vmr-codeflow-status/SKILL.md @@ -0,0 +1,228 @@ +--- +name: vmr-codeflow-status +description: Analyze VMR codeflow PR status for dotnet repositories. Use when investigating stale codeflow PRs, checking if fixes have flowed through the VMR pipeline, debugging dependency update issues in PRs authored by dotnet-maestro[bot], checking overall flow status for a repo, or diagnosing why backflow PRs are missing or blocked. +--- + +# VMR Codeflow Status + +Analyze the health of VMR codeflow PRs in both directions: +- **Backflow**: `dotnet/dotnet` → product repos (e.g., `dotnet/sdk`) +- **Forward flow**: product repos → `dotnet/dotnet` + +> 🚨 **NEVER** use `gh pr review --approve` or `--request-changes`. Only `--comment` is allowed. Approval and blocking are human-only actions. + +**Workflow**: Run the script → read the human-readable output + `[CODEFLOW_SUMMARY]` JSON → synthesize recommendations yourself. The script collects data; you generate the advice. + +## Prerequisites + +- **GitHub CLI (`gh`)** — must be installed and authenticated (`gh auth login`) +- Run scripts **from the skill directory** or use the full path to the script + +## When to Use This Skill + +Use this skill when: +- A codeflow PR (from `dotnet-maestro[bot]`) has failing tests and you need to know if it's stale +- You need to check if a specific fix has flowed through the VMR pipeline to a codeflow PR +- A PR has a Maestro staleness warning ("codeflow cannot continue") or conflict +- You need to understand what manual commits would be lost if a codeflow PR is closed +- You want to check the overall state of flow for a repo (backflow and forward flow health) +- You need to know why backflow PRs are missing or when the last VMR build was published +- You're asked questions like "is this codeflow PR up to date", "has the runtime revert reached this PR", "why is the codeflow blocked", "what is the state of flow for the sdk", "what's the flow status for net11" + +## Two Modes + +| Mode | Use When | Required Params | +|------|----------|-----------------| +| **PR analysis** | Investigating a specific codeflow PR | `-PRNumber` (and optionally `-Repository`) | +| **Flow health** (`-CheckMissing`) | Checking overall repo flow status | `-CheckMissing` (optional: `-Repository`, `-Branch`) | + +> ⚠️ **Common mistake**: Don't use `-PRNumber` and `-CheckMissing` together — they are separate modes. `-CheckMissing` scans branches discovered from open and recent backflow PRs (unless `-Branch` is provided), not a specific PR. + +## Quick Start + +```powershell +# Check codeflow PR status (most common) +./scripts/Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" + +# Trace a specific fix through the pipeline +./scripts/Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" -TraceFix "dotnet/runtime#123974" + +# Show individual VMR commits that are missing +./scripts/Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" -ShowCommits + +# Check overall flow health for a repo (backflow + forward flow) +./scripts/Get-CodeflowStatus.ps1 -Repository "dotnet/roslyn" -CheckMissing + +# Check a specific branch only +./scripts/Get-CodeflowStatus.ps1 -Repository "dotnet/sdk" -CheckMissing -Branch "main" +``` + +## Key Parameters + +| Parameter | Required | Default | Description | +|-----------|----------|---------|-------------| +| `-PRNumber` | Yes (unless `-CheckMissing`) | — | GitHub PR number to analyze | +| `-Repository` | No | `dotnet/sdk` | Target repo in `owner/repo` format | +| `-TraceFix` | No | — | Trace a repo PR through the pipeline. Format: `owner/repo#number` (e.g., `dotnet/runtime#123974`) | +| `-ShowCommits` | No | `$false` | Show individual VMR commits between PR snapshot and branch HEAD | +| `-CheckMissing` | No | `$false` | Check overall flow health: missing backflow PRs, forward flow status, and official build freshness | +| `-Branch` | No | — | With `-CheckMissing`, only check a specific branch (e.g., `main`, `release/10.0`) | + +## What the Script Does + +### PR Analysis Mode (default) + +> **Design principle**: Assess current state from primary signals first, then use Maestro comments as historical context — not the other way around. Comments tell you the history, not the present. + +1. **PR Overview** — Basic PR info, flow direction (backflow vs forward flow) +2. **Current State** — Independent assessment from primary signals: empty diff, force pushes, merge status. Produces a one-line verdict (NO-OP / IN PROGRESS / STALE / ACTIVE / MERGED / CLOSED) before reading any comments +3. **Codeflow Metadata** — Extracts VMR commit, subscription ID, build info from PR body +4. **Snapshot Validation** — Cross-references PR body commit against Version.Details.xml and branch commits to detect stale metadata +5. **Source Freshness** — Compares PR's VMR snapshot against current VMR branch HEAD; shows pending forward flow PRs +6. **PR Branch Analysis** — Categorizes commits as auto-updates vs manual; detects codeflow-like manual commits +7. **Codeflow History** — Maestro comments as historical context (conflict/staleness warnings), cross-referenced against force push timestamps to determine if issues were already addressed +8. **Traces fixes** (with `-TraceFix`) — Checks if a specific fix has flowed through VMR → codeflow PR +9. **Emits structured summary** — `[CODEFLOW_SUMMARY]` JSON block with all key facts for the agent to reason over + +> **After the script runs**, you (the agent) generate recommendations. The script collects data; you synthesize the advice. See [Generating Recommendations](#generating-recommendations) below. + +### Flow Health Mode (`-CheckMissing`) +1. **Checks official build freshness** — Queries `aka.ms` shortlinks for latest published VMR build dates per channel +2. **Scans backflow PRs** — Finds branches where a backflow PR should exist but doesn't, and checks health of open PRs (conflict/staleness/resolved status) +3. **Scans forward flow** — Checks open forward flow PRs into `dotnet/dotnet` for staleness and conflicts +4. **Produces summary** — Counts healthy/blocked/missing PRs across both directions + +> ❌ **Never assume "Unknown" health means healthy.** When `gh` API calls fail (auth, rate limiting), the script returns "Unknown" status — this is explicitly excluded from healthy/covered counts. + +> ⚠️ **aka.ms redirect behavior**: 301 is expected and treated as a valid product URL (→ ci.dot.net). Non-301 redirects (often 302, which goes to Bing) indicate an invalid URL. The script only accepts 301. + +## Interpreting Results + +### Current State (assessed first, from primary signals) +- **✅ MERGED**: PR has been merged — no action needed +- **✖️ CLOSED**: PR was closed without merging — Maestro should create a replacement +- **📭 NO-OP**: Empty diff — PR likely already resolved, changes landed via other paths +- **🔄 IN PROGRESS**: Recent force push within 24h — someone is actively working on it +- **⏳ STALE**: No activity for >3 days — may need attention +- **✅ ACTIVE**: PR has content and recent activity + +### Freshness +- **✅ Up to date**: PR has the latest VMR snapshot +- **⚠️ VMR is N commits ahead**: The PR is missing updates. Check if the missing commits contain the fix you need. +- **📊 Forward flow coverage**: Shows how many missing repos have pending forward flow PRs that would close part of the gap once merged. + +### Snapshot Validation +- **✅ Match**: PR body commit matches the branch's actual "Backflow from" commit +- **⚠️ Mismatch**: PR body is stale — the script automatically uses the branch-derived commit for freshness checks +- **ℹ️ Initial commit only**: PR body can't be verified yet (no "Backflow from" commit exists) + +### Codeflow History (Maestro comments as context) +- **✅ No warnings**: Maestro can freely update the PR +- **⚠️ Staleness warning**: A forward flow merged while this backflow PR was open. Maestro blocked further updates. +- **🔴 Conflict detected**: Maestro found merge conflicts. Shows conflicting files and `darc vmr resolve-conflict` command. +- **ℹ️ Force push after warning**: When a force push post-dates a conflict/staleness warning, the issue may already be resolved. The script cross-references timestamps automatically. + +### Manual Commits +Manual commits on the PR branch are at risk if the PR is closed or force-triggered. The script lists them so you can decide whether to preserve them. + +### Fix Tracing +When using `-TraceFix`: +- **✅ Fix is in VMR manifest**: The fix has flowed to the VMR +- **✅ Fix is in PR snapshot**: The codeflow PR already includes this fix +- **❌ Fix is NOT in PR snapshot**: The PR needs a codeflow update to get this fix + +## Generating Recommendations + +After the script outputs the `[CODEFLOW_SUMMARY]` JSON block, **you** synthesize recommendations. Do not parrot the JSON — reason over it. + +### Decision logic + +Check `isCodeflowPR` first — if `false`, skip all codeflow-specific advice: +- **Not a codeflow PR** (`isCodeflowPR = false` or `flowDirection = "unknown"`): State this clearly. No darc commands, no codeflow recommendations. Treat as a normal PR. + +Then read `currentState`: + +| State | Action | +|-------|--------| +| `MERGED` | No action needed. Mention Maestro will create a new PR if VMR has newer content. | +| `CLOSED` | Suggest triggering a new PR if `subscriptionId` is available. | +| `NO-OP` | PR has no meaningful changes. Recommend closing or merging to clear state. If `subscriptionId` is available, offer force-trigger as a third option. | +| `IN_PROGRESS` | Someone is actively working. Recommend waiting, then checking back. | +| `STALE` | Needs attention — see warnings below for what's blocking. | +| `ACTIVE` | PR is healthy — check freshness and warnings for nuance. | + +Then layer in context from `warnings`, `freshness`, and `commits`: + +- **Unresolved conflict** (`warnings.conflictCount > 0`, `conflictMayBeResolved = false`): Lead with "resolve conflicts" using `darc vmr resolve-conflict --subscription `. Offer "close & reopen" as alternative. +- **Conflict may be resolved** (`conflictMayBeResolved = true`): Note the force push post-dates the conflict warning. Suggest verifying, then merging. +- **Staleness warning active** (`stalenessCount > 0`, `stalenessMayBeResolved = false`): Codeflow is blocked. Options: merge as-is, force trigger, or close & reopen. +- **Manual commits present** (`commits.manual > 0`): Warn that force-trigger or close will lose them. If `commits.codeflowLikeManual > 0`, note the freshness gap may be partially covered. +- **Behind on freshness** (`freshness.aheadBy > 0`): Mention the PR is missing updates. If staleness is blocking, a force trigger is needed. Otherwise, Maestro should auto-update. + +### Darc commands to include + +When recommending actions, include the relevant `darc` command with the actual `subscriptionId` from the summary. Be precise about what each command does: + +| Command | What it does | When to use | +|---------|-------------|-------------| +| `darc trigger-subscriptions --id ` | Normal trigger — only works if subscription isn't stale. Creates a new PR if none exists. | PR was closed, or no PR exists | +| `darc trigger-subscriptions --id --force` | Force trigger — **overwrites the existing PR branch** with fresh VMR content. Does not create a new PR. | PR exists but is stale/no-op and you want to reuse it | +| `darc vmr resolve-conflict --subscription ` | Resolve conflicts locally and push to the PR branch | PR has merge conflicts | + +> ⚠️ **Common mistake**: Don't say "close then force-trigger" — force-trigger pushes to the *existing* PR. If you close first, use a normal trigger instead (which creates a new PR). The two paths are: (A) force-trigger to refresh the existing PR, or (B) close + normal-trigger to get a new PR. + +### Tone + +Be direct. Lead with the most important action. Use 2-4 bullet points, not long paragraphs. Include the darc command inline so the user can copy-paste. + +## Darc Commands for Remediation + +After analyzing the codeflow status, common next steps involve `darc` commands: + +```bash +# Force trigger the subscription to get a fresh codeflow update +darc trigger-subscriptions --id --force + +# Normal trigger (only works if not stale) +darc trigger-subscriptions --id + +# Check subscription details +darc get-subscriptions --target-repo dotnet/sdk --source-repo dotnet/dotnet + +# Get BAR build details +darc get-build --id + +# Resolve codeflow conflicts locally +darc vmr resolve-conflict --subscription +``` + +Install darc via `eng\common\darc-init.ps1` in any arcade-enabled repository. + +### When the script reports "Maestro may be stuck" + +When the script shows a missing backflow PR with "Maestro may be stuck" (builds are fresh but no PR was created), follow these diagnostic steps: + +1. **Check the subscription** to find when it last consumed a build: + ```bash + darc get-subscriptions --target-repo --source-repo dotnet/dotnet + ``` + Look at the `Last Build` field — if it's weeks old while the channel has newer builds, the subscription is stuck. + +2. **Compare against the latest channel build** to confirm the gap: + ```bash + darc get-latest-build --repo dotnet/dotnet --channel "" + ``` + Channel names follow patterns like `.NET 11.0.1xx SDK`, `.NET 10.0.1xx SDK`, `.NET 11.0.1xx SDK Preview 1`. + +3. **Trigger the subscription manually** to unstick it: + ```bash + darc trigger-subscriptions --id + ``` + +4. **If triggering doesn't produce a PR within a few minutes**, the issue may be deeper — check Maestro health or open an issue on `dotnet/arcade`. + +## References + +- **VMR codeflow concepts**: See [references/vmr-codeflow-reference.md](references/vmr-codeflow-reference.md) +- **VMR build topology & staleness diagnosis**: See [references/vmr-build-topology.md](references/vmr-build-topology.md) — explains how to diagnose widespread backflow staleness by checking VMR build health, the bootstrap chicken-and-egg problem, and the channel/subscription flow +- **Codeflow PR documentation**: [dotnet/dotnet Codeflow-PRs.md](https://github.com/dotnet/dotnet/blob/main/docs/Codeflow-PRs.md) diff --git a/.github/skills/vmr-codeflow-status/references/vmr-build-topology.md b/.github/skills/vmr-codeflow-status/references/vmr-build-topology.md new file mode 100644 index 000000000000..bcbc82df8246 --- /dev/null +++ b/.github/skills/vmr-codeflow-status/references/vmr-build-topology.md @@ -0,0 +1,252 @@ +# VMR Build Topology and Staleness Diagnosis + +## Overview + +When backflow PRs are missing across multiple repositories simultaneously, the root cause +is usually not Maestro — it's that the VMR can't build successfully, so no new channel +builds are produced, and subscriptions have nothing to trigger on. + +This reference explains how to diagnose that situation using publicly available signals. + +## Build Pipeline Structure + +The VMR (`dotnet/dotnet`) has two tiers of builds: + +### Public CI (validation only) +- **AzDO org**: `dnceng-public` +- **Project**: `public` (ID: `cbb18261-c48f-4abb-8651-8cdcb5474649`) +- **Pipeline**: `dotnet-unified-build` (definition 278) +- **Purpose**: Validates PRs and runs scheduled CI on `refs/heads/main` and release branches +- **Does NOT publish** to Maestro channels — cannot trigger subscriptions + +### Official builds (channel publishing) +- **AzDO org**: `dnceng` (internal, requires auth) +- **Purpose**: Produces signed builds that publish to Maestro channels (e.g., `.NET 11.0.1xx SDK`) +- **These are the builds that trigger Maestro subscriptions and create backflow PRs** +- Not queryable without internal access + +### Key insight +When investigating stale backflow, the **public CI builds are a useful proxy**. If the public +scheduled build on `refs/heads/main` is failing, the official build is almost certainly +failing too (they build the same source). A string of failed public builds strongly suggests +the official pipeline is also broken. + +## Checking Official Build Freshness (aka.ms) + +The most direct way to check if official VMR builds are producing output is to query +the SDK blob storage via `aka.ms` shortlinks. When official builds succeed, they publish +SDK artifacts to `ci.dot.net`. We can check when the latest build was published. + +### How it works + +1. Resolve the aka.ms redirect (returns 301 with the blob URL): + ``` + https://aka.ms/dotnet/{channel}/daily/dotnet-sdk-win-x64.zip + ``` + Example channels: `11.0.1xx`, `11.0.1xx-preview1`, `10.0.3xx`, `10.0.1xx` + +2. The 301 Location header gives the actual blob URL on `ci.dot.net`, which includes + the version number in the path. + +3. HEAD the blob URL — the `Last-Modified` header tells you exactly when the build was + published. + +### Example (PowerShell) + +```powershell +Add-Type -AssemblyName System.Net.Http +$handler = [System.Net.Http.HttpClientHandler]::new() +$handler.AllowAutoRedirect = $false +$client = [System.Net.Http.HttpClient]::new($handler) + +# Step 1: Resolve aka.ms → ci.dot.net blob URL +$resp = $client.GetAsync("https://aka.ms/dotnet/11.0.1xx/daily/dotnet-sdk-win-x64.zip").Result +$blobUrl = $resp.Headers.Location.ToString() # Only if StatusCode is 301 +$resp.Dispose() + +# Step 2: HEAD the blob for Last-Modified +$head = Invoke-WebRequest -Uri $blobUrl -Method Head -UseBasicParsing +$published = [DateTimeOffset]::Parse($head.Headers['Last-Modified']).UtcDateTime +$age = [DateTime]::UtcNow - $published + +$client.Dispose() +$handler.Dispose() +``` + +### Interpreting results +- **< 1 day old**: Official builds are healthy for this channel +- **1-2 days old**: Normal for daily builds, especially over weekends +- **3+ days old**: Official builds are likely failing — investigate further +- **Multiple channels stale simultaneously**: Strong signal of a systemic VMR build problem + +### Validating with darc (when auth is available) + +The aka.ms approach is an auth-free proxy. When `darc` is installed and authenticated, +you can get the authoritative answer directly from Maestro: + +```bash +# Latest build on a channel (exact match for what triggers subscriptions) +darc get-latest-build --repo dotnet/dotnet --channel ".NET 11.0.1xx SDK" + +# Check what build a subscription last acted on +darc get-subscriptions --source-repo dotnet/dotnet --target-repo dotnet/aspnetcore +``` + +The `Date Produced` from `darc get-latest-build` will be ~6 hours earlier than the +aka.ms blob `Last-Modified` (due to signing/publishing delay), but they refer to the +same build. If the subscription's `Last Build` SHA matches the channel's latest build, +then Maestro already fired — no newer builds exist. + +### Channel-to-branch mapping + +| Channel | VMR branch | Backflow targets | +|---------|-----------|-----------------| +| `11.0.1xx` | `main` | runtime, sdk, aspnetcore (main) | +| `11.0.1xx-preview1` | `release/11.0.1xx-preview1` | runtime, sdk, aspnetcore (preview) | +| `10.0.3xx` | `release/10.0.3xx` | sdk (release/10.0.3xx) | +| `10.0.2xx` | `release/10.0.2xx` | sdk (release/10.0.2xx) | +| `10.0.1xx` | `release/10.0.1xx` | runtime, sdk, aspnetcore (release/10.0) | + +### Cross-referencing with Version.Details.xml and PR metadata + +There are two sources of truth for what VMR build a repo is synced to: + +**1. `eng/Version.Details.xml` in the target repo (authoritative):** +```xml + +``` +- `Sha` = the exact VMR commit the repo is synced to +- `BarId` = the Maestro build ID (queryable via `darc get-build --id 297974` for date/channel) +- Dependency version strings encode build dates (e.g., `26069.105` → year 26, day-code 069) + +**2. Backflow PR body (when a PR is open):** +``` +- **Date Produced**: February 4, 2026 11:05:10 AM UTC +- **Build**: [20260203.11](...) ([300217](https://maestro.dot.net/channel/8298/.../build/300217)) +``` + +**Comparing against aka.ms build date:** +- If they match → the backflow PR is based on the latest successful build +- If the aka.ms build is newer → a newer build succeeded but hasn't triggered backflow yet +- If the aka.ms build matches the PR but is old → no new successful builds since + +## Querying Public VMR CI Builds + +Public CI builds (separate from official builds) can confirm whether the VMR source is +buildable. These don't publish to channels but use the same source. + +### AzDO REST API endpoints + +Recent scheduled builds on a branch: +``` +GET https://dev.azure.com/dnceng-public/public/_apis/build/builds?definitions=278&branchName=refs/heads/main&$top=5&api-version=7.0 +``` + +Last successful build: +``` +GET https://dev.azure.com/dnceng-public/public/_apis/build/builds?definitions=278&branchName=refs/heads/main&resultFilter=succeeded&$top=1&api-version=7.0 +``` + +Build timeline (to find failing jobs): +``` +GET https://dev.azure.com/dnceng-public/public/_apis/build/builds/{buildId}/timeline?api-version=7.0 +``` + +### Interpreting results +- **`reason: schedule`** — Scheduled daily builds, closest proxy to official builds +- **`reason: pullRequest`** — PR validation only +- **`result: failed`** with consecutive scheduled builds — strong signal of broken VMR +- Check the timeline for which jobs/stages failed to understand the root cause + +## Diagnosing Widespread Backflow Staleness + +### Pattern: Multiple repos missing backflow simultaneously + +When `CheckMissing` shows missing backflow across 3+ repos (e.g., runtime, SDK, aspnetcore +all stale), this is almost always a VMR build problem, not a Maestro problem. + +**Diagnosis steps:** + +1. **Check public VMR builds**: Query the last 5 scheduled builds on the affected branch. + If all are failing, the VMR build is broken. + +2. **Find the failure**: Get the timeline of the most recent failed build. Look for failed + stages/jobs — common failures include: + - **macOS signing** (SignTool crashes on non-PE files) + - **Windows build** (individual repo build failures within the VMR) + - **Source-build validation** (packaging or dependency issues) + +3. **Check for known issues**: Search `dotnet/dotnet` issues with label `[Operational Issue]` + or search for the error message. + +4. **Check the last successful build date**: A gap of days or weeks confirms the VMR has been + broken for an extended period. + +### Pattern: Single repo missing backflow + +When only one repo is missing backflow but others are healthy, the issue is more likely: +- Maestro subscription disabled or misconfigured +- The specific repo's forward flow is blocking (conflict or staleness) +- Channel mismatch + +Use `darc get-subscriptions --source-repo dotnet/dotnet --target-repo dotnet/` to check. + +## The Bootstrap / Chicken-and-Egg Problem + +The VMR builds arcade and other infrastructure from source. When an infrastructure fix +(e.g., in `dotnet/arcade`) is needed to unblock the VMR build itself, a circular dependency +can occur: + +1. Arcade fix merges in `dotnet/arcade` +2. Arcade forward-flows to VMR (`dotnet/dotnet`) +3. VMR now has the fix **in source**, but the build tooling used to build may still be the + old version (from a previous successful bootstrap) +4. The build fails because the **bootstrap SDK** (cached from a prior build) doesn't have + the fix yet + +**Resolution** (by VMR maintainers): +- Re-bootstrap: Build a new `source-built-sdks` package from a working state +- Manual intervention: Patch the bootstrap or skip the failing step +- Wait for a full re-bootstrap cycle after a milestone release + +This is not something that can be fixed by triggering subscriptions or resolving conflicts. +When you see this pattern, flag it as needing VMR infrastructure team intervention. + +## Channels and Subscription Flow + +``` +dotnet/arcade ──forward flow──► dotnet/dotnet (VMR) +dotnet/runtime ─forward flow──► dotnet/dotnet (VMR) +dotnet/sdk ────forward flow──► dotnet/dotnet (VMR) + ...other repos... + +dotnet/dotnet (VMR) + │ + ├── official build succeeds + │ │ + │ ▼ + │ publishes to channel (e.g., ".NET 11.0.1xx SDK") + │ │ + │ ▼ + │ Maestro fires subscriptions + │ │ + │ ├──► dotnet/runtime backflow PR + │ ├──► dotnet/sdk backflow PR + │ ├──► dotnet/aspnetcore backflow PR + │ └──► ...etc + │ + └── official build FAILS + │ + ▼ + nothing publishes → no subscriptions fire → all backflow stalls +``` + +## Quick Reference: Common VMR Build Failures + +| Failure | Symptom | Root cause | +|---------|---------|------------| +| SignTool crash | `Unknown file format` in Sign.proj on macOS | Non-PE file in signing input (e.g., tar.gz) | +| Repo build failure | `error MSB...` in a specific repo's build | Source incompatibility within VMR | +| Source-build validation | Packaging or prebuilt detection errors | New prebuilt dependency introduced | +| Infrastructure timeout | Build exceeds time limit | Resource contention or build perf regression | diff --git a/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md b/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md new file mode 100644 index 000000000000..cfbb5e40fb1f --- /dev/null +++ b/.github/skills/vmr-codeflow-status/references/vmr-codeflow-reference.md @@ -0,0 +1,144 @@ +# VMR Codeflow Reference + +## Key Concepts + +### Codeflow Types +- **Backflow** (VMR → product repo): Automated PRs created by Maestro that bring VMR source updates + dependency updates into product repos (e.g., `dotnet/sdk`). These are titled `[branch] Source code updates from dotnet/dotnet`. +- **Forward flow** (product repo → VMR): Changes from product repos flowing into the VMR. These are titled `[branch] Source code updates from dotnet/`. + +### Staleness +When a product repo pushes changes to the VMR (forward flow merges) while a backflow PR is already open, Maestro blocks further codeflow updates to that PR. The bot posts a warning comment with options: +1. Merge the PR as-is, then Maestro creates a new PR with remaining changes +2. Close the PR and let Maestro open a fresh one (loses manual commits) +3. Force trigger: `darc trigger-subscriptions --id --force` (manual commits may be reverted) + +### Key Files +- **`src/source-manifest.json`** (in VMR): Tracks the exact commit SHA for each product repo synchronized into the VMR. This is the authoritative source of truth. +- **`eng/Version.Details.xml`** (in product repos): Tracks dependencies and includes a `` tag for codeflow tracking. + +## PR Body Metadata Format + +Codeflow PRs have structured metadata in their body: + +``` +[marker]: <> (Begin:) +## From https://github.com/dotnet/dotnet +- **Subscription**: [](https://maestro.dot.net/subscriptions?search=) +- **Build**: []() ([]()) +- **Date Produced**: +- **Commit**: []() +- **Commit Diff**: [...]() +- **Branch**: []() +[marker]: <> (End:) +``` + +## Darc CLI Commands + +The `darc` tool (Dependency ARcade) manages dependency flow in the .NET ecosystem. Install via `eng\common\darc-init.ps1` in any arcade-enabled repo. + +### Essential Commands for Codeflow Analysis + +#### Get subscription details +```bash +# Find all subscriptions flowing to a repo +darc get-subscriptions --target-repo dotnet/sdk --source-repo dotnet/dotnet + +# Output shows subscription ID, channel, update frequency, merge policies +``` + +#### Trigger a codeflow update +```bash +# Normal trigger (only works if not stale) +darc trigger-subscriptions --id + +# Force trigger (works even when stale, but may revert manual commits) +darc trigger-subscriptions --id --force + +# Trigger with a specific build +darc trigger-subscriptions --id --build +``` + +#### Get build information +```bash +# Get BAR build details by ID (found in PR body or AzDO logs) +darc get-build --id + +# Get latest build for a repo on a channel +darc get-latest-build --repo dotnet/dotnet --channel ".NET 11 Preview 1" +``` + +#### Check subscription health +```bash +# See if dependencies are missing subscriptions or have issues +darc get-health --channel ".NET 11 Preview 1" +``` + +#### Simulate a subscription update locally +```bash +# Dry-run to see what a subscription would update +darc update-dependencies --subscription --dry-run +``` + +### VMR-Specific Commands + +```bash +# Resolve codeflow conflicts locally +darc vmr resolve-conflict --subscription --build + +# Flow source from VMR → local repo +darc vmr backflow --subscription + +# Flow source from local repo → local VMR +darc vmr forwardflow --subscription + +# Get version (SHA) of a repo in the VMR +darc vmr get-version + +# Diff VMR vs product repos +darc vmr diff +``` + +### Halting and Restarting Dependency Flow + +- **Disable default channel**: `darc default-channel-status --disable --id ` — stops new builds from flowing +- **Disable subscription**: `darc subscription-status --disable --id ` — stops flow between specific repos +- **Pin dependency**: Add `Pinned="true"` to dependency in `Version.Details.xml` — prevents specific dependency from updating + +## API Endpoints + +### GitHub API +- PR details: `GET /repos/{owner}/{repo}/pulls/{pr_number}` +- PR comments: `GET /repos/{owner}/{repo}/issues/{pr_number}/comments` +- PR commits: `GET /repos/{owner}/{repo}/pulls/{pr_number}/commits` +- Compare commits: `GET /repos/{owner}/{repo}/compare/{base}...{head}` +- File contents: `GET /repos/{owner}/{repo}/contents/{path}?ref={branch}` + +### VMR Source Manifest +``` +GET /repos/dotnet/dotnet/contents/src/source-manifest.json?ref={branch} +``` +Returns JSON with `repositories[]` array, each having `path`, `remoteUri`, `commitSha`. + +### Maestro/BAR REST API +Base URL: `https://maestro.dot.net` +- Swagger: `https://maestro.dot.net/swagger` +- Get subscriptions: `GET /api/subscriptions` +- Get builds: `GET /api/builds` +- Get build by ID: `GET /api/builds/{id}` + +## Common Scenarios + +### 1. Codeflow is stale — a fix landed but hasn't reached the PR +**Symptoms**: Tests failing on the codeflow PR; the fix is merged in a product repo. +**Diagnosis**: Compare `source-manifest.json` on VMR branch HEAD vs the PR's VMR snapshot commit. +**Resolution**: Close PR + reopen, or force trigger the subscription. + +### 2. Opposite codeflow merged — staleness warning +**Symptoms**: Maestro bot comment saying "codeflow cannot continue". +**Diagnosis**: Check PR comments for the warning. Check if forward flow PRs merged after the backflow PR was opened. +**Resolution**: Follow the options in the bot's comment. + +### 3. Manual commits on the codeflow PR +**Symptoms**: Developers added manual fixes to unblock the PR (baseline updates, workarounds). +**Diagnosis**: Analyze PR commits to identify non-maestro commits. +**Risk**: Closing the PR loses these. Force-triggering may revert them. diff --git a/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 new file mode 100644 index 000000000000..9c968ec8e993 --- /dev/null +++ b/.github/skills/vmr-codeflow-status/scripts/Get-CodeflowStatus.ps1 @@ -0,0 +1,1476 @@ +<# +.SYNOPSIS + Analyzes VMR codeflow PR status for dotnet repositories. + +.DESCRIPTION + Checks whether a codeflow PR (backflow from dotnet/dotnet VMR) is up to date, + detects staleness warnings, traces specific fixes through the pipeline, and + provides actionable recommendations. + + Can also check if a backflow PR is expected but missing for a given repo/branch. + +.PARAMETER PRNumber + GitHub PR number to analyze. Required unless -CheckMissing is used. + +.PARAMETER Repository + Target repository (default: dotnet/sdk). Format: owner/repo. + +.PARAMETER TraceFix + Optional. A repo PR to trace through the pipeline (e.g., "dotnet/runtime#123974"). + Checks if the fix has flowed through VMR into the codeflow PR. + +.PARAMETER ShowCommits + Show individual VMR commits between the PR snapshot and current branch HEAD. + +.PARAMETER CheckMissing + Check if backflow PRs are expected but missing for a repository. When used, + PRNumber is not required. Finds the most recent merged backflow PR for each branch, + extracts its VMR commit, and compares against current VMR branch HEAD. + +.PARAMETER Branch + Optional. When used with -CheckMissing, only check a specific branch instead of all. + +.EXAMPLE + ./Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" + +.EXAMPLE + ./Get-CodeflowStatus.ps1 -PRNumber 52727 -Repository "dotnet/sdk" -TraceFix "dotnet/runtime#123974" + +.EXAMPLE + ./Get-CodeflowStatus.ps1 -Repository "dotnet/roslyn" -CheckMissing + +.EXAMPLE + ./Get-CodeflowStatus.ps1 -Repository "dotnet/roslyn" -CheckMissing -Branch "main" +#> + +param( + [int]$PRNumber, + + [string]$Repository = "dotnet/sdk", + + [string]$TraceFix, + + [switch]$ShowCommits, + + [switch]$CheckMissing, + + [string]$Branch +) + +$ErrorActionPreference = "Stop" + +# --- Helpers --- + +function Invoke-GitHubApi { + param( + [string]$Endpoint, + [switch]$Raw + ) + try { + $args = @($Endpoint) + if ($Raw) { + $args += '-H' + $args += 'Accept: application/vnd.github.raw' + } + $result = gh api @args 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Warning "GitHub API call failed: $Endpoint" + return $null + } + if ($Raw) { return $result -join "`n" } + return ($result -join "`n") | ConvertFrom-Json + } + catch { + Write-Warning "Error calling GitHub API: $_" + return $null + } +} + +function Get-ShortSha { + param([string]$Sha, [int]$Length = 12) + if (-not $Sha) { return "(unknown)" } + return $Sha.Substring(0, [Math]::Min($Length, $Sha.Length)) +} + +function Write-Section { + param([string]$Title) + Write-Host "" + Write-Host "=== $Title ===" -ForegroundColor Cyan +} + +function Write-Status { + param([string]$Label, [string]$Value, [string]$Color = "White") + Write-Host " ${Label}: " -NoNewline + Write-Host $Value -ForegroundColor $Color +} + +# Check an open codeflow PR for staleness/conflict warnings +# Returns a hashtable with: Status, Color, HasConflict, HasStaleness, WasResolved +function Get-CodeflowPRHealth { + param([int]$PRNumber, [string]$Repo = "dotnet/dotnet") + + $result = @{ Status = "⚠️ Unknown"; Color = "Yellow"; HasConflict = $false; HasStaleness = $false; WasResolved = $false; Details = @() } + + $prJson = gh pr view $PRNumber -R $Repo --json body,comments,updatedAt,mergeable 2>$null + if ($LASTEXITCODE -ne 0 -or -not $prJson) { return $result } + + try { $prDetail = ($prJson -join "`n") | ConvertFrom-Json } catch { return $result } + + # If we got here, we can determine health + $result.Status = "✅ Healthy" + $result.Color = "Green" + + $hasConflict = $false + $hasStaleness = $false + if ($prDetail.comments) { + foreach ($comment in $prDetail.comments) { + if ($comment.author.login -match '^dotnet-maestro') { + if ($comment.body -match 'codeflow cannot continue|the source repository has received code changes') { $hasStaleness = $true } + if ($comment.body -match 'Conflict detected') { $hasConflict = $true } + } + } + } + + $wasConflict = $hasConflict + $wasStaleness = $hasStaleness + + # If issues detected, check if they were resolved + # Two signals: (1) PR is mergeable (no git conflict), (2) Codeflow verification SUCCESS + # Either one clears the conflict flag. Staleness needs a newer commit after the warning. + if ($hasConflict -or $hasStaleness) { + # Check mergeable status — if PR has no git conflicts, clear the conflict flag + $isMergeable = $false + if ($prDetail.PSObject.Properties.Name -contains 'mergeable' -and $prDetail.mergeable -eq 'MERGEABLE') { + $isMergeable = $true + } + if ($isMergeable -and $hasConflict) { + $hasConflict = $false + } + + $checksJson = gh pr checks $PRNumber -R $Repo --json name,state 2>$null + if ($LASTEXITCODE -eq 0 -and $checksJson) { + try { + $checks = ($checksJson -join "`n") | ConvertFrom-Json + $codeflowCheck = @($checks | Where-Object { $_.name -match 'Codeflow verification' }) | Select-Object -First 1 + if (($codeflowCheck -and $codeflowCheck.state -eq 'SUCCESS') -or $isMergeable) { + # No merge conflict — either Codeflow verification passes or PR is mergeable + $hasConflict = $false + # For staleness, check if there are commits after the last staleness warning + if ($hasStaleness) { + $commitsJson = gh pr view $PRNumber -R $Repo --json commits --jq '.commits[-1].committedDate' 2>$null + if ($LASTEXITCODE -eq 0 -and $commitsJson) { + $lastCommitTime = ($commitsJson -join "").Trim() + $lastWarnTime = $null + foreach ($comment in $prDetail.comments) { + if ($comment.author.login -match '^dotnet-maestro' -and $comment.body -match 'codeflow cannot continue|the source repository has received code changes') { + $warnDt = [DateTimeOffset]::Parse($comment.createdAt).UtcDateTime + if (-not $lastWarnTime -or $warnDt -gt $lastWarnTime) { + $lastWarnTime = $warnDt + } + } + } + $commitDt = if ($lastCommitTime) { [DateTimeOffset]::Parse($lastCommitTime).UtcDateTime } else { $null } + if ($lastWarnTime -and $commitDt -and $commitDt -gt $lastWarnTime) { + $hasStaleness = $false + } + } + } + } + } catch { } + } + } + + if ($hasConflict) { + $result.Status = "🔴 Conflict" + $result.Color = "Red" + $result.HasConflict = $true + } + elseif ($hasStaleness) { + $result.Status = "⚠️ Stale" + $result.Color = "Yellow" + $result.HasStaleness = $true + } + else { + if ($wasConflict) { $result.Status = "✅ Conflict resolved"; $result.WasResolved = $true } + elseif ($wasStaleness) { $result.Status = "✅ Updated since staleness warning"; $result.WasResolved = $true } + } + + return $result +} + +function Get-VMRBuildFreshness { + param([string]$VMRBranch) + + # Map VMR branch to aka.ms channel + $channel = $null + $blobUrl = $null + + Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue + $handler = [System.Net.Http.HttpClientHandler]::new() + $handler.AllowAutoRedirect = $false + $client = [System.Net.Http.HttpClient]::new($handler) + $client.Timeout = [TimeSpan]::FromSeconds(15) + + try { + if ($VMRBranch -eq "main") { + $tryChannels = @("11.0.1xx", "12.0.1xx", "10.0.1xx") + foreach ($ch in $tryChannels) { + try { + $resp = $client.GetAsync("https://aka.ms/dotnet/$ch/daily/dotnet-sdk-win-x64.zip").Result + if ([int]$resp.StatusCode -eq 301 -and $resp.Headers.Location) { + $channel = $ch + $blobUrl = $resp.Headers.Location.ToString() + $resp.Dispose() + break + } + $resp.Dispose() + } catch { } + } + } + elseif ($VMRBranch -match 'release/(\d+\.\d+\.\d+xx-preview\.?\d+)') { + # aka.ms uses "preview1" not "preview.1" + $channel = $Matches[1] -replace 'preview\.', 'preview' + } + elseif ($VMRBranch -match 'release/(\d+\.\d+)\.(\d)xx') { + $channel = "$($Matches[1]).$($Matches[2])xx" + } + + if (-not $channel) { return $null } + + if (-not $blobUrl) { + $resp = $client.GetAsync("https://aka.ms/dotnet/$channel/daily/dotnet-sdk-win-x64.zip").Result + if ([int]$resp.StatusCode -ne 301 -or -not $resp.Headers.Location) { + $resp.Dispose() + return $null + } + $blobUrl = $resp.Headers.Location.ToString() + $resp.Dispose() + } + + $version = if ($blobUrl -match '/Sdk/([^/]+)/') { $Matches[1] } else { $null } + + # Use HttpClient HEAD (consistent with above, avoids mixing Invoke-WebRequest) + # Need a separate client with auto-redirect enabled for the blob URL + $blobHandler = [System.Net.Http.HttpClientHandler]::new() + $blobClient = [System.Net.Http.HttpClient]::new($blobHandler) + $blobClient.Timeout = [TimeSpan]::FromSeconds(15) + $published = $null + try { + $request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Head, $blobUrl) + $headResp = $blobClient.SendAsync($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).Result + # PowerShell unwraps Nullable — use cast, not .Value + $lastMod = $headResp.Content.Headers.LastModified + if ($null -eq $lastMod) { $lastMod = $headResp.Headers.LastModified } + if ($null -ne $lastMod) { $published = ([DateTimeOffset]$lastMod).UtcDateTime } + } + finally { + if ($headResp) { $headResp.Dispose() } + if ($request) { $request.Dispose() } + $blobClient.Dispose() + $blobHandler.Dispose() + } + + if (-not $published) { return $null } + return @{ + Channel = $channel + Version = $version + Published = $published + Age = [DateTime]::UtcNow - $published + } + } + catch { + return $null + } + finally { + if ($client) { $client.Dispose() } + } +} + +# --- Parse repo owner/name --- +if ($Repository -notmatch '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$') { + Write-Error "Repository must be in format 'owner/repo' (e.g., 'dotnet/sdk')" + return +} + +# --- CheckMissing mode: find expected but missing backflow PRs --- +if ($CheckMissing) { + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Error "GitHub CLI (gh) is not installed or not in PATH. Install from https://cli.github.com/" + return + } + + Write-Section "Checking for missing backflow PRs in $Repository" + + # dotnet/dotnet doesn't have backflow from itself — skip to forward flow + build freshness + if ($Repository -eq "dotnet/dotnet") { + Write-Host " ℹ️ VMR (dotnet/dotnet) does not have backflow from itself" -ForegroundColor DarkGray + + # Still show build freshness for the VMR + $vmrBranches = @{} + if ($Branch -eq "main" -or -not $Branch) { $vmrBranches["main"] = "main" } + if ($Branch -match 'release/' -or -not $Branch) { + # Try to detect release branches + $branchesJson = gh api "/repos/dotnet/dotnet/branches?per_page=30" --jq '.[].name' 2>$null + if ($LASTEXITCODE -eq 0 -and $branchesJson) { + foreach ($b in ($branchesJson -split "`n")) { + if ($b -match '^release/') { $vmrBranches[$b] = $b } + } + } + } + if ($vmrBranches.Count -gt 0) { + Write-Section "Official Build Freshness (via aka.ms)" + $checkedChannels = @{} + $anyVeryStale = $false + foreach ($entry in $vmrBranches.GetEnumerator()) { + $freshness = Get-VMRBuildFreshness -VMRBranch $entry.Value + if ($freshness -and -not $checkedChannels.ContainsKey($freshness.Channel)) { + $checkedChannels[$freshness.Channel] = $freshness + $ageDays = $freshness.Age.TotalDays + $ageStr = if ($ageDays -ge 1) { "$([math]::Round($ageDays, 1))d" } else { "$([math]::Round($freshness.Age.TotalHours, 1))h" } + $color = if ($ageDays -gt 3) { 'Red' } elseif ($ageDays -gt 1) { 'Yellow' } else { 'Green' } + $versionStr = if ($freshness.Version) { $freshness.Version } else { "unknown" } + $branchLabel = "$($entry.Key) → $($freshness.Channel)" + Write-Host " $($branchLabel.PadRight(40)) $($versionStr.PadRight(48)) $($freshness.Published.ToString('yyyy-MM-dd HH:mm')) UTC ($ageStr ago)" -ForegroundColor $color + if ($ageDays -gt 3) { $anyVeryStale = $true } + } + } + if ($anyVeryStale) { + Write-Host "" + Write-Host " ⚠️ Official builds appear stale — VMR may be failing to build" -ForegroundColor Yellow + Write-Host " Check https://dev.azure.com/dnceng-public/public/_build?definitionId=278 for public CI failures" -ForegroundColor DarkGray + Write-Host " See also: https://github.com/dotnet/dotnet/issues?q=is:issue+is:open+%22Operational+Issue%22" -ForegroundColor DarkGray + } + } + return + } + + # Find open backflow PRs (to know which branches are already covered) + $openPRsJson = gh search prs --repo $Repository --author "dotnet-maestro[bot]" --state open "Source code updates from dotnet/dotnet" --json number,title --limit 50 2>$null + $openPRs = @() + $ghSearchFailed = $false + if ($LASTEXITCODE -eq 0 -and $openPRsJson) { + try { $openPRs = ($openPRsJson -join "`n") | ConvertFrom-Json } catch { $openPRs = @() } + } + elseif ($LASTEXITCODE -ne 0) { + Write-Warning "gh search failed (exit code $LASTEXITCODE). Check authentication with 'gh auth status'." + $ghSearchFailed = $true + } + $openBranches = @{} + foreach ($opr in $openPRs) { + if ($opr.title -match '^\[([^\]]+)\]') { + $openBranches[$Matches[1]] = $opr.number + } + } + + if ($openPRs.Count -gt 0) { + Write-Host " Open backflow PRs already exist:" -ForegroundColor White + foreach ($opr in $openPRs) { + Write-Host " #$($opr.number): $($opr.title)" -ForegroundColor Green + } + Write-Host "" + } + + # Find recently merged backflow PRs to discover branches and VMR commit mapping + $mergedPRsJson = gh search prs --repo $Repository --author "dotnet-maestro[bot]" --state closed --merged "Source code updates from dotnet/dotnet" --limit 30 --sort updated --json number,title,closedAt 2>$null + $mergedPRs = @() + if ($LASTEXITCODE -eq 0 -and $mergedPRsJson) { + try { $mergedPRs = ($mergedPRsJson -join "`n") | ConvertFrom-Json } catch { $mergedPRs = @() } + } + elseif ($LASTEXITCODE -ne 0 -and -not $ghSearchFailed) { + Write-Warning "gh search for merged PRs failed (exit code $LASTEXITCODE). Results may be incomplete." + } + + if ($mergedPRs.Count -eq 0 -and $openPRs.Count -eq 0) { + if ($ghSearchFailed) { + Write-Host " ❌ Could not query GitHub. Check 'gh auth status' and rate limits." -ForegroundColor Red + } + else { + Write-Host " No backflow PRs found (open or recently merged). This repo may not have backflow subscriptions." -ForegroundColor Yellow + } + return + } + + # Group merged PRs by branch, keeping only the most recently merged per branch + $branchLastMerged = @{} + foreach ($mpr in $mergedPRs) { + if ($mpr.title -match '^\[([^\]]+)\]') { + $branchName = $Matches[1] + if ($Branch -and $branchName -ne $Branch) { continue } + if (-not $branchLastMerged.ContainsKey($branchName)) { + $branchLastMerged[$branchName] = $mpr + } + else { + # Keep the one with the later closedAt (actual merge time) + $existing = $branchLastMerged[$branchName] + if ($mpr.closedAt -and $existing.closedAt -and $mpr.closedAt -gt $existing.closedAt) { + $branchLastMerged[$branchName] = $mpr + } + } + } + } + + if ($Branch -and -not $branchLastMerged.ContainsKey($Branch) -and -not $openBranches.ContainsKey($Branch)) { + Write-Host " No backflow PRs found for branch '$Branch'." -ForegroundColor Yellow + return + } + + # For each branch without an open PR, check if VMR has moved past the last merged commit + $missingCount = 0 + $coveredCount = 0 + $upToDateCount = 0 + $blockedCount = 0 + $vmrBranchesFound = @{} + $cachedPRBodies = @{} + + # First pass: collect VMR branch mappings from merged PRs (needed for build freshness) + foreach ($branchName in ($branchLastMerged.Keys | Sort-Object)) { + if ($openBranches.ContainsKey($branchName)) { continue } + $lastPR = $branchLastMerged[$branchName] + $prDetailJson = gh pr view $lastPR.number -R $Repository --json body 2>$null + if ($LASTEXITCODE -ne 0) { continue } + try { $prDetail = ($prDetailJson -join "`n") | ConvertFrom-Json } catch { continue } + $cachedPRBodies[$branchName] = $prDetail + $vmrBranchFromPR = $null + if ($prDetail.body -match '\*\*Branch\*\*:\s*\[([^\]]+)\]') { $vmrBranchFromPR = $Matches[1] } + if ($vmrBranchFromPR) { $vmrBranchesFound[$branchName] = $vmrBranchFromPR } + } + + # --- Official build freshness check (shown first for context) --- + $buildsAreStale = $false + if ($vmrBranchesFound.Count -gt 0) { + Write-Section "Official Build Freshness (via aka.ms)" + $checkedChannels = @{} + foreach ($entry in $vmrBranchesFound.GetEnumerator()) { + $freshness = Get-VMRBuildFreshness -VMRBranch $entry.Value + if ($freshness -and -not $checkedChannels.ContainsKey($freshness.Channel)) { + $checkedChannels[$freshness.Channel] = $freshness + $ageDays = $freshness.Age.TotalDays + $ageStr = if ($ageDays -ge 1) { "$([math]::Round($ageDays, 1))d" } else { "$([math]::Round($freshness.Age.TotalHours, 1))h" } + $color = if ($ageDays -gt 3) { 'Red' } elseif ($ageDays -gt 1) { 'Yellow' } else { 'Green' } + $versionStr = if ($freshness.Version) { $freshness.Version } else { "unknown" } + $branchLabel = "$($entry.Key) → $($freshness.Channel)" + Write-Host " $($branchLabel.PadRight(40)) $($versionStr.PadRight(48)) $($freshness.Published.ToString('yyyy-MM-dd HH:mm')) UTC ($ageStr ago)" -ForegroundColor $color + if ($ageDays -gt 3) { $buildsAreStale = $true } + } + } + if ($buildsAreStale) { + Write-Host "" + Write-Host " ⚠️ Official builds appear stale — VMR may be failing to build" -ForegroundColor Yellow + Write-Host " Missing backflow PRs below are likely caused by this, not a Maestro issue" -ForegroundColor DarkGray + Write-Host " Check https://dev.azure.com/dnceng-public/public/_build?definitionId=278 for public CI failures" -ForegroundColor DarkGray + Write-Host " See also: https://github.com/dotnet/dotnet/issues?q=is:issue+is:open+%22Operational+Issue%22" -ForegroundColor DarkGray + } + } + + # --- Per-branch backflow analysis --- + Write-Section "Backflow status ($Repository ← dotnet/dotnet)" + + foreach ($branchName in ($branchLastMerged.Keys | Sort-Object)) { + $lastPR = $branchLastMerged[$branchName] + Write-Host "" + Write-Host " Branch: $branchName" -ForegroundColor White + + if ($openBranches.ContainsKey($branchName)) { + $bfHealth = Get-CodeflowPRHealth -PRNumber $openBranches[$branchName] -Repo $Repository + Write-Host " Open backflow PR #$($openBranches[$branchName]): $($bfHealth.Status)" -ForegroundColor $bfHealth.Color + if ($bfHealth.HasConflict -or $bfHealth.HasStaleness) { $blockedCount++ } + elseif ($bfHealth.Status -notlike '*Unknown*') { $coveredCount++ } + continue + } + + # Get the PR body to extract VMR commit (branch already collected above) + $vmrBranchFromPR = $vmrBranchesFound[$branchName] + if (-not $vmrBranchFromPR) { + Write-Host " ⚠️ Could not determine VMR branch from last merged PR" -ForegroundColor Yellow + continue + } + + # Use cached PR body from first pass + $prDetail = $cachedPRBodies[$branchName] + if (-not $prDetail) { + Write-Host " ⚠️ Could not fetch PR details" -ForegroundColor Yellow + continue + } + + $vmrCommitFromPR = $null + if ($prDetail.body -match '\*\*Commit\*\*:\s*\[([a-fA-F0-9]+)\]') { + $vmrCommitFromPR = $Matches[1] + } + + if (-not $vmrCommitFromPR) { + Write-Host " ⚠️ Could not parse VMR commit from last merged PR #$($lastPR.number)" -ForegroundColor Yellow + continue + } + + Write-Host " Last merged: PR #$($lastPR.number) on $($lastPR.closedAt)" -ForegroundColor DarkGray + Write-Host " VMR branch: $vmrBranchFromPR" -ForegroundColor DarkGray + Write-Host " VMR commit: $(Get-ShortSha $vmrCommitFromPR)" -ForegroundColor DarkGray + + # Get current VMR branch HEAD + $encodedVmrBranch = [uri]::EscapeDataString($vmrBranchFromPR) + $vmrHead = Invoke-GitHubApi "/repos/dotnet/dotnet/commits/$encodedVmrBranch" + if (-not $vmrHead) { + Write-Host " ⚠️ Could not fetch VMR branch HEAD for $vmrBranchFromPR" -ForegroundColor Yellow + continue + } + + $vmrHeadSha = $vmrHead.sha + $vmrHeadDate = $vmrHead.commit.committer.date + + if ($vmrCommitFromPR -eq $vmrHeadSha -or $vmrHeadSha.StartsWith($vmrCommitFromPR) -or $vmrCommitFromPR.StartsWith($vmrHeadSha)) { + Write-Host " ✅ VMR branch is at same commit — no backflow needed" -ForegroundColor Green + $upToDateCount++ + } + else { + # Check how far ahead + $compare = Invoke-GitHubApi "/repos/dotnet/dotnet/compare/$vmrCommitFromPR...$vmrHeadSha" + $ahead = if ($compare) { $compare.ahead_by } else { "?" } + + Write-Host " 🔴 MISSING BACKFLOW PR" -ForegroundColor Red + Write-Host " VMR is $ahead commit(s) ahead since last merged PR" -ForegroundColor Yellow + Write-Host " VMR HEAD: $(Get-ShortSha $vmrHeadSha) ($vmrHeadDate)" -ForegroundColor DarkGray + Write-Host " Last merged VMR commit: $(Get-ShortSha $vmrCommitFromPR)" -ForegroundColor DarkGray + + # Check how long ago the last PR merged + $mergedTime = [DateTimeOffset]::Parse($lastPR.closedAt).UtcDateTime + $elapsed = [DateTime]::UtcNow - $mergedTime + if ($elapsed.TotalHours -gt 6) { + if ($buildsAreStale) { + Write-Host " ℹ️ No new official build available — backflow blocked upstream" -ForegroundColor DarkGray + } + else { + Write-Host " ⚠️ Last PR merged $([math]::Round($elapsed.TotalHours, 1)) hours ago — Maestro may be stuck" -ForegroundColor Yellow + } + } + else { + Write-Host " ℹ️ Last PR merged $([math]::Round($elapsed.TotalHours, 1)) hours ago — Maestro may still be processing" -ForegroundColor DarkGray + } + $missingCount++ + } + } + + # Also check open-only branches (that weren't in merged list) + foreach ($branchName in ($openBranches.Keys | Sort-Object)) { + if (-not $branchLastMerged.ContainsKey($branchName)) { + if ($Branch -and $branchName -ne $Branch) { continue } + Write-Host "" + Write-Host " Branch: $branchName" -ForegroundColor White + $bfHealth = Get-CodeflowPRHealth -PRNumber $openBranches[$branchName] -Repo $Repository + Write-Host " Open backflow PR #$($openBranches[$branchName]): $($bfHealth.Status)" -ForegroundColor $bfHealth.Color + if ($bfHealth.HasConflict -or $bfHealth.HasStaleness) { $blockedCount++ } + elseif ($bfHealth.Status -notlike '*Unknown*') { $coveredCount++ } + } + } + + # --- Forward flow: check PRs from this repo into the VMR --- + $repoShortName = $Repository -replace '^dotnet/', '' + Write-Host "" + Write-Section "Forward flow PRs ($Repository → dotnet/dotnet)" + + $fwdPRsJson = gh search prs --repo dotnet/dotnet --author "dotnet-maestro[bot]" --state open "Source code updates from dotnet/$repoShortName" --json number,title --limit 10 2>$null + $fwdPRs = @() + if ($LASTEXITCODE -eq 0 -and $fwdPRsJson) { + try { $fwdPRs = ($fwdPRsJson -join "`n") | ConvertFrom-Json } catch { $fwdPRs = @() } + } + # Filter to exact repo match (avoid dotnet/sdk matching dotnet/sdk-container-builds) + $fwdPRs = @($fwdPRs | Where-Object { $_.title -match "from dotnet/$([regex]::Escape($repoShortName))$" }) + + $fwdHealthy = 0 + $fwdStale = 0 + $fwdConflict = 0 + + if ($fwdPRs.Count -eq 0) { + Write-Host " No open forward flow PRs found" -ForegroundColor DarkGray + } + else { + foreach ($fpr in $fwdPRs) { + $fprBranch = if ($fpr.title -match '^\[([^\]]+)\]') { $Matches[1] } else { "unknown" } + if ($Branch -and $fprBranch -ne $Branch) { continue } + + $fwdHealth = Get-CodeflowPRHealth -PRNumber $fpr.number -Repo "dotnet/dotnet" + + if ($fwdHealth.HasConflict) { $fwdConflict++ } + elseif ($fwdHealth.HasStaleness) { $fwdStale++ } + elseif ($fwdHealth.Status -notlike '*Unknown*') { $fwdHealthy++ } + + Write-Host " PR #$($fpr.number) [$fprBranch]: $($fwdHealth.Status)" -ForegroundColor $fwdHealth.Color + Write-Host " https://github.com/dotnet/dotnet/pull/$($fpr.number)" -ForegroundColor DarkGray + } + } + + Write-Section "Summary" + Write-Host " Backflow ($Repository ← dotnet/dotnet):" -ForegroundColor White + if ($coveredCount -gt 0) { Write-Host " Branches with healthy open PRs: $coveredCount" -ForegroundColor Green } + if ($upToDateCount -gt 0) { Write-Host " Branches up to date: $upToDateCount" -ForegroundColor Green } + if ($blockedCount -gt 0) { Write-Host " Branches with blocked open PRs: $blockedCount" -ForegroundColor Red } + if ($missingCount -gt 0) { + Write-Host " Branches MISSING backflow PRs: $missingCount" -ForegroundColor Red + } + if ($missingCount -eq 0 -and $blockedCount -eq 0) { + Write-Host " No missing backflow PRs ✅" -ForegroundColor Green + } + Write-Host " Forward flow ($Repository → dotnet/dotnet):" -ForegroundColor White + if ($fwdPRs.Count -eq 0) { + Write-Host " No open forward flow PRs" -ForegroundColor DarkGray + } + else { + if ($fwdHealthy -gt 0) { Write-Host " Healthy: $fwdHealthy" -ForegroundColor Green } + if ($fwdStale -gt 0) { Write-Host " Stale: $fwdStale" -ForegroundColor Yellow } + if ($fwdConflict -gt 0) { Write-Host " Conflicted: $fwdConflict" -ForegroundColor Red } + } + return +} + +# --- Validate PRNumber for non-CheckMissing mode --- +if (-not $PRNumber) { + Write-Error "PRNumber is required unless -CheckMissing is used." + return +} + +# --- Step 1: PR Overview --- +Write-Section "Codeflow PR #$PRNumber in $Repository" + +if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Error "GitHub CLI (gh) is not installed or not in PATH. Install from https://cli.github.com/" + return +} + +$prJson = gh pr view $PRNumber -R $Repository --json body,title,state,author,headRefName,baseRefName,createdAt,updatedAt,url,comments,commits,additions,deletions,changedFiles +if ($LASTEXITCODE -ne 0) { + Write-Error "Could not fetch PR #$PRNumber from $Repository. Ensure you are authenticated (gh auth login)." + return +} +$pr = ($prJson -join "`n") | ConvertFrom-Json + +Write-Status "Title" $pr.title +Write-Status "State" $pr.state +Write-Status "Branch" "$($pr.headRefName) -> $($pr.baseRefName)" +Write-Status "Created" $pr.createdAt +Write-Status "Updated" $pr.updatedAt +Write-Host " URL: $($pr.url)" + +# Check if this is actually a codeflow PR and detect flow direction +$isMaestroPR = $pr.author.login -eq "dotnet-maestro[bot]" +$isBackflow = $pr.title -match "Source code updates from dotnet/dotnet" +$isForwardFlow = $pr.title -match "Source code updates from (dotnet/\S+)" -and -not $isBackflow +if (-not $isMaestroPR -and -not $isBackflow -and -not $isForwardFlow) { + Write-Warning "This does not appear to be a codeflow PR (author: $($pr.author.login), title: $($pr.title))" + Write-Warning "Expected author 'dotnet-maestro[bot]' and title containing 'Source code updates from'" +} + +if ($isForwardFlow) { + $sourceRepo = $Matches[1] + Write-Status "Flow" "Forward ($sourceRepo → $Repository)" "Cyan" +} +elseif ($isBackflow) { + Write-Status "Flow" "Backflow (dotnet/dotnet → $Repository)" "Cyan" +} + +# --- Step 2: Current State (independent assessment from primary signals) --- +Write-Section "Current State" + +# Check for empty diff (0 changed files) +$isEmptyDiff = ($pr.changedFiles -eq 0 -and $pr.additions -eq 0 -and $pr.deletions -eq 0) +if ($isEmptyDiff) { + Write-Host " 📭 Empty diff: 0 changed files, 0 additions, 0 deletions" -ForegroundColor Yellow +} + +# Check PR timeline for force pushes +$forcePushEvents = @() +$owner, $repo = $Repository -split '/' +$forcePushFetchSucceeded = $false +try { + $timelineJson = gh api "repos/$owner/$repo/issues/$PRNumber/timeline" --paginate --slurp --jq 'map(.[] | select(.event == "head_ref_force_pushed"))' 2>$null + if ($LASTEXITCODE -eq 0 -and $timelineJson) { + $forcePushEvents = @($timelineJson | ConvertFrom-Json) + $forcePushFetchSucceeded = $true + } elseif ($LASTEXITCODE -ne 0) { + Write-Warning "Could not fetch PR timeline for force push detection (gh api exit code $LASTEXITCODE). Current state assessment may be incomplete." + } else { + $forcePushFetchSucceeded = $true + } +} +catch { + Write-Warning "Failed to parse timeline JSON for force push events: $($_.Exception.Message)" + $forcePushEvents = @() +} + +if ($forcePushEvents.Count -gt 0) { + foreach ($fp in $forcePushEvents) { + $fpActor = if ($fp.actor) { $fp.actor.login } else { "unknown" } + $fpTime = $fp.created_at + $fpSha = if ($fp.commit_id) { Get-ShortSha $fp.commit_id } else { "unknown" } + Write-Host " 🔄 Force push by @$fpActor at $fpTime (→ $fpSha)" -ForegroundColor Cyan + } + $lastForcePush = $forcePushEvents[-1] + $lastForcePushTime = if ($lastForcePush.created_at) { + [DateTimeOffset]::Parse($lastForcePush.created_at).UtcDateTime + } else { $null } + $lastForcePushActor = if ($lastForcePush.actor) { $lastForcePush.actor.login } else { "unknown" } +} + +# Synthesize current state assessment +$prUpdatedTime = if ($pr.updatedAt) { [DateTimeOffset]::Parse($pr.updatedAt).UtcDateTime } else { $null } +$prAgeDays = if ($prUpdatedTime) { ([DateTime]::UtcNow - $prUpdatedTime).TotalDays } else { 0 } +$isClosed = $pr.state -eq "CLOSED" +$isMerged = $pr.state -eq "MERGED" +$currentState = if ($isMerged) { + "MERGED" +} elseif ($isClosed) { + "CLOSED" +} elseif ($isEmptyDiff) { + "NO-OP" +} elseif ($forcePushEvents.Count -gt 0 -and $lastForcePushTime -and ([DateTime]::UtcNow - $lastForcePushTime).TotalHours -lt 24) { + "IN_PROGRESS" +} elseif ($prAgeDays -gt 3) { + "STALE" +} else { + "ACTIVE" +} + +Write-Host "" +switch ($currentState) { + "MERGED" { Write-Host " ✅ MERGED — PR has been merged" -ForegroundColor Green } + "CLOSED" { Write-Host " ✖️ CLOSED — PR was closed without merging" -ForegroundColor DarkGray } + "NO-OP" { Write-Host " 📭 NO-OP — empty diff, likely already resolved" -ForegroundColor Yellow } + "IN_PROGRESS" { Write-Host " 🔄 IN PROGRESS — recent force push, awaiting update" -ForegroundColor Cyan } + "STALE" { Write-Host " ⏳ STALE — no recent activity" -ForegroundColor Yellow } + "ACTIVE" { Write-Host " ✅ ACTIVE — PR has content" -ForegroundColor Green } +} + +# --- Step 3: Codeflow Metadata --- +Write-Section "Codeflow Metadata" + +$body = $pr.body + +# Extract subscription ID +$subscriptionId = $null +if ($body -match '\(Begin:([a-f0-9-]+)\)') { + $subscriptionId = $Matches[1] + Write-Status "Subscription" $subscriptionId +} + +# Extract source commit (VMR commit for backflow, repo commit for forward flow) +$sourceCommit = $null +if ($body -match '\*\*Commit\*\*:\s*\[([a-fA-F0-9]+)\]') { + $sourceCommit = $Matches[1] + $commitLabel = if ($isForwardFlow) { "Source Commit" } else { "VMR Commit" } + Write-Status $commitLabel $sourceCommit +} +# Keep $vmrCommit alias for backflow compatibility +$vmrCommit = $sourceCommit + +# Extract build info +if ($body -match '\*\*Build\*\*:\s*\[([^\]]+)\]\(([^\)]+)\)') { + Write-Status "Build" "$($Matches[1])" + Write-Status "Build URL" $Matches[2] +} + +# Extract date produced +if ($body -match '\*\*Date Produced\*\*:\s*(.+)') { + Write-Status "Date Produced" $Matches[1].Trim() +} + +# Extract source branch +$vmrBranch = $null +if ($body -match '\*\*Branch\*\*:\s*\[([^\]]+)\]') { + $vmrBranch = $Matches[1] + $branchLabel = if ($isForwardFlow) { "Source Branch" } else { "VMR Branch" } + Write-Status $branchLabel $vmrBranch +} + +# Extract commit diff +if ($body -match '\*\*Commit Diff\*\*:\s*\[([^\]]+)\]\(([^\)]+)\)') { + Write-Status "Commit Diff" $Matches[1] +} + +# Extract associated repo changes from footer +$repoChanges = @() +$changeMatches = [regex]::Matches($body, '- (https://github\.com/([^/]+/[^/]+)/compare/([a-fA-F0-9]+)\.\.\.([a-fA-F0-9]+))') +foreach ($m in $changeMatches) { + $repoChanges += @{ + URL = $m.Groups[1].Value + Repo = $m.Groups[2].Value + FromSha = $m.Groups[3].Value + ToSha = $m.Groups[4].Value + } +} +if ($repoChanges.Count -gt 0) { + Write-Status "Associated Repos" "$($repoChanges.Count) repos with source changes" +} + +if (-not $vmrCommit -or -not $vmrBranch) { + Write-Warning "Could not parse VMR metadata from PR body. This may not be a codeflow PR." + if (-not $vmrBranch) { + # For backflow: infer from PR target (which is the product repo branch = VMR branch name) + # For forward flow: infer from PR head branch pattern or source repo context + if ($isForwardFlow) { + $vmrBranch = $pr.headRefName -replace '^darc-', '' -replace '-[a-f0-9-]+$', '' + if (-not $vmrBranch) { $vmrBranch = $pr.baseRefName } + } + else { + $vmrBranch = $pr.baseRefName + } + Write-Status "Inferred Branch" "$vmrBranch (from PR metadata)" + } +} + +# For backflow: compare against VMR (dotnet/dotnet) branch HEAD +# For forward flow: compare against product repo branch HEAD +$freshnessRepo = if ($isForwardFlow) { $sourceRepo } else { "dotnet/dotnet" } +$freshnessRepoLabel = if ($isForwardFlow) { $sourceRepo } else { "VMR" } + +# Pre-load PR commits for use in validation and later analysis +$prCommits = $pr.commits + +# --- Step 4: Determine actual VMR snapshot on the PR branch --- +# Priority: 1) Version.Details.xml (ground truth), 2) commit messages, 3) PR body +$branchVmrCommit = $null +$commitMsgVmrCommit = $null +$versionDetailsVmrCommit = $null + +# First: check eng/Version.Details.xml on the PR branch (authoritative source) +if (-not $isForwardFlow) { + $vdContent = Invoke-GitHubApi "/repos/$Repository/contents/eng/Version.Details.xml?ref=$([System.Uri]::EscapeDataString($pr.headRefName))" -Raw + if ($vdContent) { + try { + [xml]$vdXml = $vdContent + $sourceNode = $vdXml.Dependencies.Source + if ($sourceNode -and $sourceNode.Sha -and $sourceNode.Sha -match '^[a-fA-F0-9]{40}$') { + $versionDetailsVmrCommit = $sourceNode.Sha + $branchVmrCommit = $versionDetailsVmrCommit + } + } + catch { + # Fall back to regex if XML parsing fails + if ($vdContent -match ']*Sha="([a-fA-F0-9]{40})"') { + $versionDetailsVmrCommit = $Matches[1] + $branchVmrCommit = $versionDetailsVmrCommit + } + } + } +} + +# Second: scan commit messages for "Backflow from" / "Forward flow from" SHAs +if ($prCommits) { + $reversedCommits = @($prCommits) + [Array]::Reverse($reversedCommits) + foreach ($c in $reversedCommits) { + $msg = $c.messageHeadline + if ($msg -match '(?:Backflow|Forward flow) from .+ / ([a-fA-F0-9]+)') { + $commitMsgVmrCommit = $Matches[1] + break + } + } + # For forward flow (no Version.Details.xml source), commit messages are primary + if (-not $branchVmrCommit -and $commitMsgVmrCommit) { + $branchVmrCommit = $commitMsgVmrCommit + } +} + +if ($branchVmrCommit -or $vmrCommit) { + Write-Section "Snapshot Validation" + $usedBranchSnapshot = $false + + if ($branchVmrCommit) { + # We have a branch-derived snapshot (from Version.Details.xml or commit message) + $branchShort = Get-ShortSha $branchVmrCommit + $sourceLabel = if ($versionDetailsVmrCommit -and $branchVmrCommit -eq $versionDetailsVmrCommit) { "Version.Details.xml" } else { "branch commit" } + + if ($vmrCommit) { + $bodyShort = Get-ShortSha $vmrCommit + if ($vmrCommit.StartsWith($branchVmrCommit, [StringComparison]::OrdinalIgnoreCase) -or $branchVmrCommit.StartsWith($vmrCommit, [StringComparison]::OrdinalIgnoreCase)) { + Write-Host " ✅ $sourceLabel ($branchShort) matches PR body ($bodyShort)" -ForegroundColor Green + } + else { + Write-Host " ⚠️ MISMATCH: $sourceLabel has $branchShort but PR body claims $bodyShort" -ForegroundColor Red + Write-Host " PR body is stale — using $sourceLabel for freshness check" -ForegroundColor Yellow + } + } + else { + Write-Host " ℹ️ PR body has no commit reference — using $sourceLabel ($branchShort)" -ForegroundColor Yellow + } + + # Resolve to full SHA for accurate comparison (skip API call if already full-length) + if ($branchVmrCommit.Length -ge 40) { + $vmrCommit = $branchVmrCommit + $usedBranchSnapshot = $true + } + else { + $resolvedCommit = Invoke-GitHubApi "/repos/$freshnessRepo/commits/$branchVmrCommit" + if ($resolvedCommit) { + $vmrCommit = $resolvedCommit.sha + $usedBranchSnapshot = $true + } + elseif ($vmrCommit) { + Write-Host " ⚠️ Could not resolve $sourceLabel SHA $branchShort — falling back to PR body ($(Get-ShortSha $vmrCommit))" -ForegroundColor Yellow + } + else { + Write-Host " ⚠️ Could not resolve $sourceLabel SHA $branchShort" -ForegroundColor Yellow + } + } + } + else { + # No branch-derived snapshot — PR body only + $commitCount = if ($prCommits) { $prCommits.Count } else { 0 } + if ($commitCount -eq 1 -and $prCommits[0].messageHeadline -match "^Initial commit for subscription") { + Write-Host " ℹ️ PR has only an initial subscription commit — PR body snapshot ($(Get-ShortSha $vmrCommit)) not yet verifiable" -ForegroundColor DarkGray + } + else { + Write-Host " ⚠️ Could not verify PR body snapshot ($(Get-ShortSha $vmrCommit)) from branch" -ForegroundColor Yellow + } + } +} + +# --- Step 5: Check source freshness --- +$freshnessLabel = if ($isForwardFlow) { "Source Freshness" } else { "VMR Freshness" } +Write-Section $freshnessLabel + +$sourceHeadSha = $null +$aheadBy = 0 +$behindBy = 0 +$compareStatus = $null + +if ($vmrCommit -and $vmrBranch) { + # Get current branch HEAD (URL-encode branch name for path segments with /) + $encodedBranch = [uri]::EscapeDataString($vmrBranch) + $branchHead = Invoke-GitHubApi "/repos/$freshnessRepo/commits/$encodedBranch" + if ($branchHead) { + $sourceHeadSha = $branchHead.sha + $sourceHeadDate = $branchHead.commit.committer.date + $snapshotSource = if ($usedBranchSnapshot) { + if ($versionDetailsVmrCommit -and $vmrCommit.StartsWith($versionDetailsVmrCommit, [StringComparison]::OrdinalIgnoreCase)) { "from Version.Details.xml" } + elseif ($commitMsgVmrCommit) { "from branch commit" } + else { "from branch" } + } else { "from PR body" } + Write-Status "PR snapshot" "$(Get-ShortSha $vmrCommit) ($snapshotSource)" + Write-Status "$freshnessRepoLabel HEAD" "$(Get-ShortSha $sourceHeadSha) ($sourceHeadDate)" + + if ($vmrCommit -eq $sourceHeadSha) { + Write-Host " ✅ PR is up to date with $freshnessRepoLabel branch" -ForegroundColor Green + } + else { + # Compare to find how many commits differ + $compare = Invoke-GitHubApi "/repos/$freshnessRepo/compare/$vmrCommit...$sourceHeadSha" + if ($compare) { + $aheadBy = $compare.ahead_by + $behindBy = $compare.behind_by + $compareStatus = $compare.status + + switch ($compareStatus) { + 'identical' { + Write-Host " ✅ PR is up to date with $freshnessRepoLabel branch" -ForegroundColor Green + } + 'ahead' { + Write-Host " ⚠️ $freshnessRepoLabel is $aheadBy commit(s) ahead of the PR snapshot" -ForegroundColor Yellow + } + 'behind' { + Write-Host " ⚠️ $freshnessRepoLabel is $behindBy commit(s) behind the PR snapshot" -ForegroundColor Yellow + } + 'diverged' { + Write-Host " ⚠️ $freshnessRepoLabel and PR snapshot have diverged: $aheadBy commit(s) ahead and $behindBy commit(s) behind" -ForegroundColor Yellow + } + default { + Write-Host " ⚠️ $freshnessRepoLabel and PR snapshot differ (status: $compareStatus)" -ForegroundColor Yellow + } + } + + if ($compare.total_commits -and $compare.commits) { + $returnedCommits = @($compare.commits).Count + if ($returnedCommits -lt $compare.total_commits) { + Write-Host " ⚠️ Compare API returned $returnedCommits of $($compare.total_commits) commits; listing may be incomplete." -ForegroundColor Yellow + } + } + + if ($ShowCommits -and $compare.commits) { + Write-Host "" + $commitLabel = switch ($compareStatus) { + 'ahead' { "Commits since PR snapshot:" } + 'behind' { "Commits in PR snapshot but not in $freshnessRepoLabel`:" } + default { "Commits differing:" } + } + Write-Host " $commitLabel" -ForegroundColor Yellow + foreach ($c in $compare.commits) { + $msg = ($c.commit.message -split "`n")[0] + if ($msg.Length -gt 100) { $msg = $msg.Substring(0, 97) + "..." } + $date = $c.commit.committer.date + Write-Host " $(Get-ShortSha $c.sha 8) $date $msg" + } + } + + # Check which repos have updates in the missing commits + $missingRepoUpdates = @() + if ($compare.commits) { + foreach ($c in $compare.commits) { + $msg = ($c.commit.message -split "`n")[0] + if ($msg -match 'Source code updates from ([^\s(]+)') { + $missingRepoUpdates += $Matches[1] + } + } + } + if ($missingRepoUpdates.Count -gt 0) { + $uniqueRepos = $missingRepoUpdates | Select-Object -Unique + Write-Host "" + Write-Host " Missing updates from: $($uniqueRepos -join ', ')" -ForegroundColor Yellow + } + + # --- For backflow PRs that are behind: check pending forward flow PRs --- + if ($isBackflow -and $compareStatus -eq 'ahead' -and $aheadBy -gt 0 -and $vmrBranch) { + $forwardPRsJson = gh search prs --repo dotnet/dotnet --author "dotnet-maestro[bot]" --state open "Source code updates from" --base $vmrBranch --json number,title --limit 20 2>$null + $pendingForwardPRs = @() + if ($LASTEXITCODE -eq 0 -and $forwardPRsJson) { + try { + $allForward = ($forwardPRsJson -join "`n") | ConvertFrom-Json + # Filter to forward flow PRs (not backflow) targeting this VMR branch + $pendingForwardPRs = $allForward | Where-Object { + $_.title -match "Source code updates from (dotnet/\S+)" -and + $Matches[1] -ne "dotnet/dotnet" + } + } + catch { + Write-Warning "Failed to parse forward flow PR search results. Skipping forward flow analysis." + } + } + + if ($pendingForwardPRs.Count -gt 0) { + Write-Host "" + Write-Host " Pending forward flow PRs into VMR ($vmrBranch):" -ForegroundColor Cyan + + $coveredRepos = @() + foreach ($fpr in $pendingForwardPRs) { + $fprSourceRepo = $null + if ($fpr.title -match "Source code updates from (dotnet/\S+)") { + $fprSourceRepo = $Matches[1] + } + $coveredLabel = "" + if ($fprSourceRepo -and $uniqueRepos -contains $fprSourceRepo) { + $coveredRepos += $fprSourceRepo + $coveredLabel = " ← covers missing updates" + } + Write-Host " dotnet/dotnet#$($fpr.number): $($fpr.title)$coveredLabel" -ForegroundColor DarkGray + } + + if ($coveredRepos.Count -gt 0) { + $uncoveredRepos = $uniqueRepos | Where-Object { $_ -notin $coveredRepos } + $coveredCount = $coveredRepos.Count + $totalMissing = $uniqueRepos.Count + Write-Host "" + Write-Host " 📊 Forward flow coverage: $coveredCount of $totalMissing missing repo(s) have pending forward flow PRs" -ForegroundColor Cyan + if ($uncoveredRepos.Count -gt 0) { + Write-Host " Still waiting on: $($uncoveredRepos -join ', ')" -ForegroundColor Yellow + } + else { + Write-Host " ✅ All missing repos have pending forward flow — gap should close once they merge + new backflow triggers" -ForegroundColor Green + } + } + } + } + } + } + } +} +else { + Write-Warning "Cannot check freshness without source commit and branch info" +} + +# Collect Maestro comment data (needed by PR Branch Analysis and Codeflow History) +$stalenessWarnings = @() +$lastStalenessComment = $null + +if ($pr.comments) { + foreach ($comment in $pr.comments) { + $commentAuthor = $comment.author.login + if ($commentAuthor -eq "dotnet-maestro[bot]" -or $commentAuthor -eq "dotnet-maestro") { + if ($comment.body -match "codeflow cannot continue" -or $comment.body -match "darc trigger-subscriptions") { + $stalenessWarnings += $comment + $lastStalenessComment = $comment + } + } + } +} + +$conflictWarnings = @() +$lastConflictComment = $null + +if ($pr.comments) { + foreach ($comment in $pr.comments) { + $commentAuthor = $comment.author.login + if ($commentAuthor -eq "dotnet-maestro[bot]" -or $commentAuthor -eq "dotnet-maestro") { + if ($comment.body -match "Conflict detected") { + $conflictWarnings += $comment + $lastConflictComment = $comment + } + } + } +} + +# Extract conflicting files (used in History and Recommendations) +$conflictFiles = @() +if ($lastConflictComment) { + $fileMatches = [regex]::Matches($lastConflictComment.body, '-\s+`([^`]+)`\s*\r?\n') + foreach ($fm in $fileMatches) { + $conflictFiles += $fm.Groups[1].Value + } +} + +# Cross-reference force push against conflict/staleness warnings (data only) +$conflictMayBeResolved = $false +$stalenessMayBeResolved = $false +if ($lastForcePushTime) { + if ($conflictWarnings.Count -gt 0 -and $lastConflictComment) { + $lastConflictTime = [DateTimeOffset]::Parse($lastConflictComment.createdAt).UtcDateTime + if ($lastForcePushTime -gt $lastConflictTime) { + $conflictMayBeResolved = $true + } + } + if ($stalenessWarnings.Count -gt 0 -and $lastStalenessComment) { + $lastStalenessTime = [DateTimeOffset]::Parse($lastStalenessComment.createdAt).UtcDateTime + if ($lastForcePushTime -gt $lastStalenessTime) { + $stalenessMayBeResolved = $true + } + } +} + +# --- Step 6: PR Branch Analysis --- +Write-Section "PR Branch Analysis" + +if ($prCommits) { + $maestroCommits = @() + $manualCommits = @() + $mergeCommits = @() + + foreach ($c in $prCommits) { + $msg = $c.messageHeadline + $authorLogin = if ($c.authors -and $c.authors.Count -gt 0) { $c.authors[0].login } else { $null } + $authorName = if ($c.authors -and $c.authors.Count -gt 0) { $c.authors[0].name } else { "unknown" } + $author = if ($authorLogin) { $authorLogin } else { $authorName } + + if ($msg -match "^Merge branch") { + $mergeCommits += $c + } + elseif ($author -in @("dotnet-maestro[bot]", "dotnet-maestro") -or $msg -eq "Update dependencies") { + $maestroCommits += $c + } + else { + $manualCommits += $c + } + } + + Write-Status "Total commits" $prCommits.Count + Write-Status "Maestro auto-updates" $maestroCommits.Count + Write-Status "Merge commits" $mergeCommits.Count + Write-Status "Manual commits" $manualCommits.Count "$(if ($manualCommits.Count -gt 0) { 'Yellow' } else { 'Green' })" + + if ($manualCommits.Count -gt 0) { + Write-Host "" + Write-Host " Manual commits (at risk if PR is closed/force-triggered):" -ForegroundColor Yellow + foreach ($c in $manualCommits) { + $msg = $c.messageHeadline + if ($msg.Length -gt 80) { $msg = $msg.Substring(0, 77) + "..." } + $authorName = if ($c.authors -and $c.authors.Count -gt 0) { $c.authors[0].name } else { "unknown" } + Write-Host " $(Get-ShortSha $c.oid 8) [$authorName] $msg" + } + } + + # Detect manual commits that look like codeflow-like changes (someone manually + # doing what Maestro would do while flow is paused) + $codeflowLikeManualCommits = @() + foreach ($c in $manualCommits) { + $msg = $c.messageHeadline + if ($msg -match 'Update dependencies' -or + $msg -match 'Version\.Details\.xml' -or + $msg -match 'Versions\.props' -or + $msg -match '[Bb]ackflow' -or + $msg -match '[Ff]orward flow' -or + $msg -match 'from dotnet/' -or + $msg -match '[a-f0-9]{7,40}' -or + $msg -match 'src/SourceBuild') { + $codeflowLikeManualCommits += $c + } + } + + if ($codeflowLikeManualCommits.Count -gt 0 -and $stalenessWarnings.Count -gt 0) { + Write-Host "" + Write-Host " ⚠️ $($codeflowLikeManualCommits.Count) manual commit(s) appear to contain codeflow-like changes while flow is paused" -ForegroundColor Yellow + Write-Host " The freshness gap reported above may be partially covered by these manual updates" -ForegroundColor DarkGray + } +} + +# --- Step 7: Codeflow History (Maestro comments as historical context) --- +Write-Section "Codeflow History" +Write-Host " Maestro warnings (historical — see Current State for present status):" -ForegroundColor DarkGray + +if ($stalenessWarnings.Count -gt 0 -or $conflictWarnings.Count -gt 0) { + if ($conflictWarnings.Count -gt 0) { + Write-Host " 🔴 Conflict detected ($($conflictWarnings.Count) conflict warning(s))" -ForegroundColor Red + Write-Status "Latest conflict" $lastConflictComment.createdAt + + if ($conflictFiles.Count -gt 0) { + Write-Host " Conflicting files:" -ForegroundColor Yellow + foreach ($f in $conflictFiles) { + Write-Host " - $f" -ForegroundColor Yellow + } + } + + # Extract VMR commit from the conflict comment + if ($lastConflictComment.body -match 'sources from \[`([a-fA-F0-9]+)`\]') { + Write-Host " Conflicting VMR commit: $($Matches[1])" -ForegroundColor DarkGray + } + + # Extract resolve command + if ($lastConflictComment.body -match '(darc vmr resolve-conflict --subscription [a-fA-F0-9-]+(?:\s+--build [a-fA-F0-9-]+)?)') { + Write-Host "" + Write-Host " Resolve command:" -ForegroundColor White + Write-Host " $($Matches[1])" -ForegroundColor DarkGray + } + } + + if ($stalenessWarnings.Count -gt 0) { + if ($conflictWarnings.Count -gt 0) { Write-Host "" } + Write-Host " ⚠️ Staleness warning detected ($($stalenessWarnings.Count) warning(s))" -ForegroundColor Yellow + Write-Status "Latest warning" $lastStalenessComment.createdAt + $oppositeFlow = if ($isForwardFlow) { "backflow from VMR merged into $sourceRepo" } else { "forward flow merged into VMR" } + Write-Host " Opposite codeflow ($oppositeFlow) while this PR was open." -ForegroundColor Yellow + Write-Host " Maestro has blocked further codeflow updates to this PR." -ForegroundColor Yellow + + # Extract darc commands from the warning + if ($lastStalenessComment.body -match 'darc trigger-subscriptions --id ([a-fA-F0-9-]+)(?:\s+--force)?') { + Write-Host "" + Write-Host " Suggested commands from Maestro:" -ForegroundColor White + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-fA-F0-9-]+)\s*\r?\n') { + Write-Host " Normal trigger: $($Matches[1])" + } + if ($lastStalenessComment.body -match '(darc trigger-subscriptions --id [a-fA-F0-9-]+ --force)') { + Write-Host " Force trigger: $($Matches[1])" + } + } + } +} +else { + Write-Host " ✅ No staleness or conflict warnings found" -ForegroundColor Green +} + +# Cross-reference force push against conflict/staleness warnings (historical context) +if ($lastForcePushTime) { + if ($conflictMayBeResolved) { + Write-Host "" + Write-Host " ℹ️ Force push by @$lastForcePushActor at $($lastForcePush.created_at) is AFTER the last conflict warning" -ForegroundColor Cyan + Write-Host " Conflict may have been resolved via darc vmr resolve-conflict" -ForegroundColor DarkGray + } + if ($stalenessMayBeResolved) { + Write-Host " ℹ️ Force push is AFTER the staleness warning — someone may have acted on it" -ForegroundColor Cyan + } + if ($isEmptyDiff -and ($conflictMayBeResolved -or $stalenessMayBeResolved)) { + Write-Host "" + Write-Host " 📭 PR has empty diff after force push — codeflow changes may already be in target branch" -ForegroundColor Yellow + Write-Host " This PR is likely a no-op. Consider merging to clear state or closing it." -ForegroundColor DarkGray + } +} + +# --- Step 8: Trace a specific fix (optional) --- +if ($TraceFix) { + Write-Section "Tracing Fix: $TraceFix" + + # Parse TraceFix format: "owner/repo#number" or "repo#number" + $traceMatch = [regex]::Match($TraceFix, '(?:([^/]+)/)?([^#]+)#(\d+)') + if (-not $traceMatch.Success) { + Write-Warning "Could not parse TraceFix format. Expected: 'owner/repo#number' or 'repo#number'" + } + else { + $traceOwner = if ($traceMatch.Groups[1].Value) { $traceMatch.Groups[1].Value } else { "dotnet" } + $traceRepo = $traceMatch.Groups[2].Value + $traceNumber = $traceMatch.Groups[3].Value + $traceFullRepo = "$traceOwner/$traceRepo" + + # Check if the fix PR is merged (use merged_at since REST may not include merged boolean) + $fixPR = Invoke-GitHubApi "/repos/$traceFullRepo/pulls/$traceNumber" + $fixIsMerged = $false + if ($fixPR) { + $fixIsMerged = $null -ne $fixPR.merged_at + Write-Status "Fix PR" "${traceFullRepo}#${traceNumber}: $($fixPR.title)" + Write-Status "State" $fixPR.state + Write-Status "Merged" "$(if ($fixIsMerged) { '✅ Yes' } else { '❌ No' })" "$(if ($fixIsMerged) { 'Green' } else { 'Red' })" + if ($fixIsMerged) { + Write-Status "Merged at" $fixPR.merged_at + Write-Status "Merge commit" $fixPR.merge_commit_sha + $fixMergeCommit = $fixPR.merge_commit_sha + } + } + + # Check if the fix is in the VMR source-manifest.json on the target branch + # For forward flow, the VMR target is the PR base branch; for backflow, use $vmrBranch + $vmrManifestBranch = if ($isForwardFlow -and $pr.baseRefName) { $pr.baseRefName } else { $vmrBranch } + if ($fixIsMerged -and $vmrManifestBranch) { + Write-Host "" + Write-Host " Checking VMR source-manifest.json on $vmrManifestBranch..." -ForegroundColor White + + $encodedManifestBranch = [uri]::EscapeDataString($vmrManifestBranch) + $manifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$encodedManifestBranch" + $manifestJson = Invoke-GitHubApi $manifestUrl -Raw + if ($manifestJson) { + try { + $manifest = $manifestJson | ConvertFrom-Json + } + catch { + Write-Warning "Could not parse VMR source-manifest.json: $_" + $manifest = $null + } + + # Find the repo in the manifest + $escapedRepo = [regex]::Escape($traceRepo) + $repoEntry = $manifest.repositories | Where-Object { + $_.remoteUri -match "${escapedRepo}(\.git)?$" -or $_.path -eq $traceRepo + } + + if ($repoEntry) { + $manifestCommit = $repoEntry.commitSha + Write-Status "VMR manifest commit" "$(Get-ShortSha $manifestCommit) for $($repoEntry.path)" + + # Check if the fix merge commit is an ancestor of the manifest commit + if ($fixMergeCommit -eq $manifestCommit) { + Write-Host " ✅ Fix merge commit IS the VMR manifest commit" -ForegroundColor Green + } + else { + # Check if fix is an ancestor of the manifest commit + $ancestorCheck = Invoke-GitHubApi "/repos/$traceFullRepo/compare/$fixMergeCommit...$manifestCommit" + if ($ancestorCheck) { + if ($ancestorCheck.status -eq "ahead" -or $ancestorCheck.status -eq "identical") { + Write-Host " ✅ Fix is included in VMR manifest (manifest is ahead or identical)" -ForegroundColor Green + } + elseif ($ancestorCheck.status -eq "behind") { + Write-Host " ❌ Fix is NOT in VMR manifest yet (manifest is behind the fix)" -ForegroundColor Red + } + else { + Write-Host " ⚠️ Fix and manifest have diverged (status: $($ancestorCheck.status))" -ForegroundColor Yellow + } + } + } + + # Now check if the PR's VMR snapshot includes this + # For backflow: $vmrCommit is a VMR SHA, use it directly + # For forward flow: $vmrCommit is a source repo SHA, use PR head commit in dotnet/dotnet instead + $snapshotRef = $vmrCommit + if ($isForwardFlow -and $pr.commits -and $pr.commits.Count -gt 0) { + $snapshotRef = $pr.commits[-1].oid + } + if ($snapshotRef) { + Write-Host "" + Write-Host " Checking if fix is in the PR's snapshot..." -ForegroundColor White + + $snapshotManifestUrl = "/repos/dotnet/dotnet/contents/src/source-manifest.json?ref=$snapshotRef" + $snapshotJson = Invoke-GitHubApi $snapshotManifestUrl -Raw + if ($snapshotJson) { + try { + $snapshotData = $snapshotJson | ConvertFrom-Json + } + catch { + Write-Warning "Could not parse snapshot manifest: $_" + $snapshotData = $null + } + + $snapshotEntry = $snapshotData.repositories | Where-Object { + $_.remoteUri -match "${escapedRepo}(\.git)?$" -or $_.path -eq $traceRepo + } + + if ($snapshotEntry) { + $snapshotCommit = $snapshotEntry.commitSha + Write-Status "PR snapshot commit" "$(Get-ShortSha $snapshotCommit) for $($snapshotEntry.path)" + + if ($snapshotCommit -eq $fixMergeCommit) { + Write-Host " ✅ Fix IS in the PR's VMR snapshot" -ForegroundColor Green + } + else { + $snapshotCheck = Invoke-GitHubApi "/repos/$traceFullRepo/compare/$fixMergeCommit...$snapshotCommit" + if ($snapshotCheck) { + if ($snapshotCheck.status -eq "ahead" -or $snapshotCheck.status -eq "identical") { + Write-Host " ✅ Fix is included in PR snapshot" -ForegroundColor Green + } + else { + Write-Host " ❌ Fix is NOT in the PR's VMR snapshot" -ForegroundColor Red + Write-Host " The PR needs a codeflow update to pick up this fix." -ForegroundColor Yellow + } + } + } + } + } + } + } + else { + Write-Warning "Could not find $traceRepo in VMR source-manifest.json" + } + } + } + } +} + +# --- Step 9: Structured Summary --- +# Emit a JSON summary for the agent to reason over when generating recommendations. +# The agent should use SKILL.md guidance to synthesize contextual recommendations. + +$summary = [ordered]@{ + prNumber = $PRNumber + repository = $Repository + prState = $pr.state + currentState = $currentState + isCodeflowPR = ($isBackflow -or $isForwardFlow) + isMaestroAuthored = $isMaestroPR + flowDirection = if ($isForwardFlow) { "forward" } elseif ($isBackflow) { "backflow" } else { "unknown" } + isEmptyDiff = $isEmptyDiff + changedFiles = [int]$pr.changedFiles + additions = [int]$pr.additions + deletions = [int]$pr.deletions + subscriptionId = $subscriptionId + vmrCommit = if ($vmrCommit) { Get-ShortSha $vmrCommit } else { $null } + vmrBranch = $vmrBranch +} + +# Freshness +$hasFreshnessData = ($null -ne $vmrCommit -and $null -ne $sourceHeadSha) +$summary.freshness = [ordered]@{ + sourceHeadSha = if ($sourceHeadSha) { Get-ShortSha $sourceHeadSha } else { $null } + compareStatus = $compareStatus + aheadBy = $aheadBy + behindBy = $behindBy + isUpToDate = if ($hasFreshnessData) { ($vmrCommit -eq $sourceHeadSha -or $compareStatus -eq 'identical') } else { $null } +} + +# Force pushes +$summary.forcePushes = [ordered]@{ + count = $forcePushEvents.Count + fetchSucceeded = $forcePushFetchSucceeded + lastActor = if ($lastForcePushActor) { $lastForcePushActor } else { $null } + lastTime = if ($lastForcePushTime) { $lastForcePushTime.ToString("o") } else { $null } +} + +# Warnings +$summary.warnings = [ordered]@{ + conflictCount = $conflictWarnings.Count + conflictFiles = $conflictFiles + conflictMayBeResolved = $conflictMayBeResolved + stalenessCount = $stalenessWarnings.Count + stalenessMayBeResolved = $stalenessMayBeResolved +} + +# Commits +$manualCommitCount = if ($manualCommits) { $manualCommits.Count } else { 0 } +$codeflowLikeCount = if ($codeflowLikeManualCommits) { $codeflowLikeManualCommits.Count } else { 0 } +$summary.commits = [ordered]@{ + total = if ($prCommits) { $prCommits.Count } else { 0 } + manual = $manualCommitCount + codeflowLikeManual = $codeflowLikeCount +} + +# PR age +$summary.age = [ordered]@{ + daysSinceUpdate = [math]::Max(0, [math]::Round($prAgeDays, 1)) + createdAt = $pr.createdAt + updatedAt = $pr.updatedAt +} + +Write-Host "" +Write-Host "[CODEFLOW_SUMMARY]" +Write-Host ($summary | ConvertTo-Json -Depth 4 -Compress) +Write-Host "[/CODEFLOW_SUMMARY]" + +# Ensure clean exit code (gh api failures may leave $LASTEXITCODE = 1) +exit 0 diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 771578cd0fb0..361dd5c4b1d1 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -23,6 +23,17 @@ jobs: with: global-json-file: global.json + - name: Install .NET agent skills + continue-on-error: true + run: | + copilot plugin marketplace add dotnet/skills || true + copilot plugin install dotnet@dotnet-agent-skills || true + copilot plugin install dotnet-diag@dotnet-agent-skills || true + copilot plugin install dotnet-msbuild@dotnet-agent-skills || true + copilot plugin install dotnet-nuget@dotnet-agent-skills || true + copilot plugin install dotnet-test@dotnet-agent-skills || true + copilot plugin list || true + # Restore dependencies - this is critical for Copilot to understand the project structure - name: Restore dependencies continue-on-error: true @@ -38,6 +49,7 @@ jobs: run: | dotnet tool install --global dotnet-format dotnet tool install --global complog + dotnet tool install --global roslyn-language-server --prerelease # Setup PATH to include .NET tools - name: Setup PATH @@ -57,5 +69,9 @@ jobs: ls -la echo "=== Build artifacts ===" ls -la artifacts/ || true + echo "=== Copilot personal skills ===" + find "$HOME/.copilot/skills" -mindepth 1 -maxdepth 1 -type d | sort || true + echo "=== Roslyn language server ===" + roslyn-language-server --help | head -n 5 || true echo "=== Global.json ===" cat global.json diff --git a/.github/workflows/labeler-predict-pulls.yml b/.github/workflows/labeler-predict-pulls.yml index 9fae31e42bc1..4810385c9d2c 100644 --- a/.github/workflows/labeler-predict-pulls.yml +++ b/.github/workflows/labeler-predict-pulls.yml @@ -24,12 +24,17 @@ on: branches: - main + # Poll for open pull requests that need labels every 5 minutes + schedule: + - cron: "*/5 * * * *" + # Allow dispatching the workflow via the Actions UI, specifying ranges of numbers + # If no pull request numbers are provided, it behaves as a polling event workflow_dispatch: inputs: pulls: - description: "Pull Request Numbers (comma-separated list of ranges)." - required: true + description: "Pull Request Numbers (comma-separated list of ranges). Leave empty to poll." + required: false cache_key: description: "The cache key suffix to use for restoring the model. Defaults to 'ACTIVE'." required: true @@ -43,15 +48,70 @@ env: THRESHOLD: 0.40 jobs: + poll-pull-requests: + # Run on schedule trigger or workflow_dispatch without PR numbers, within the 'dotnet' org + if: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.pulls == '')) && github.repository_owner == 'dotnet' }} + runs-on: ubuntu-latest + permissions: + actions: read + pull-requests: read + outputs: + pulls: ${{ steps.get-pulls.outputs.pulls }} + steps: + - name: "Get open pull requests needing labels" + id: get-pulls + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + # Get the last successful schedule run's timestamp (minus 5 minutes for overlap) + last_run=$(gh run list --repo ${{ github.repository }} --workflow "${{ github.workflow }}" --event schedule --status success --limit 1 --json updatedAt --jq '.[0].updatedAt // empty') + + if [ -n "$last_run" ]; then + # Subtract 5 minutes from the last run timestamp for overlap + since=$(date -u -d "$last_run - 5 minutes" +"%Y-%m-%dT%H:%M:%SZ") + echo "Filtering PRs updated since: $since (last run: $last_run)" + pulls=$(gh pr list --repo ${{ github.repository }} --state open --json number,labels,updatedAt --limit 1000 --search "updated:>=$since") + else + # No previous run found; get all open pull requests + echo "No previous schedule run found. Getting all open pull requests." + pulls=$(gh pr list --repo ${{ github.repository }} --state open --json number,labels --limit 1000) + fi + + # Filter to PRs that don't have a label starting with LABEL_PREFIX + needs_label=$(echo "$pulls" | jq -r --arg prefix "${{ env.LABEL_PREFIX }}" ' + [.[] | select( + (.labels | map(.name) | any(startswith($prefix)) | not) + ) | .number] | join(",") + ') + + echo "Pull requests needing labels: $needs_label" + echo "pulls=$needs_label" >> $GITHUB_OUTPUT + predict-pull-label: + # The 'if' uses always() so this job runs even when poll-pull-requests is skipped # Do not automatically run the workflow on forks outside the 'dotnet' org - if: ${{ github.event_name == 'workflow_dispatch' || github.repository_owner == 'dotnet' }} + if: ${{ always() && (github.event_name == 'workflow_dispatch' || github.repository_owner == 'dotnet') }} + needs: [poll-pull-requests] runs-on: ubuntu-latest permissions: pull-requests: write steps: + - name: "Determine pull requests to process" + id: determine-pulls + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ -n "${{ inputs.pulls }}" ]; then + pulls="${{ inputs.pulls }}" + elif [ "${{ github.event_name }}" == "workflow_dispatch" ] || [ "${{ github.event_name }}" == "schedule" ]; then + pulls="${{ needs.poll-pull-requests.outputs.pulls }}" + else + pulls="${{ github.event.number }}" + fi + echo "pulls=$pulls" >> $GITHUB_OUTPUT + echo "Processing pull requests: $pulls" + - name: "Restore pulls model from cache" id: restore-model + if: ${{ steps.determine-pulls.outputs.pulls != '' }} uses: dotnet/issue-labeler/restore@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: type: pulls @@ -60,10 +120,10 @@ jobs: - name: "Predict pull labels" id: prediction - if: ${{ steps.restore-model.outputs.cache-hit == 'true' }} + if: ${{ steps.determine-pulls.outputs.pulls != '' && steps.restore-model.outputs.cache-hit == 'true' }} uses: dotnet/issue-labeler/predict@46125e85e6a568dc712f358c39f35317366f5eed # v2.0.0 with: - pulls: ${{ inputs.pulls || github.event.number }} + pulls: ${{ steps.determine-pulls.outputs.pulls }} label_prefix: ${{ env.LABEL_PREFIX }} threshold: ${{ env.THRESHOLD }} env: diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index e8da6c14eef6..02b3455cc5e7 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Check if command invoker has write access id: check-invoker-access - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ @@ -37,7 +37,7 @@ jobs: - name: Check if command invoker is Microsoft org member id: check-invoker-org if: steps.check-invoker-access.outputs.has-access == 'true' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | try { @@ -56,7 +56,7 @@ jobs: - name: Block unauthorized users if: steps.check-invoker-access.outputs.has-access != 'true' || steps.check-invoker-org.outputs.is-member != 'true' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | await github.rest.issues.createComment({ @@ -69,7 +69,7 @@ jobs: - name: Get PR author details id: pr-author - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const { data: pr } = await github.rest.pulls.get({ @@ -100,7 +100,7 @@ jobs: - name: Check if PR author is Microsoft org member id: pr-author-org - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const prAuthor = '${{ steps.pr-author.outputs.username }}'; @@ -120,7 +120,7 @@ jobs: - name: Parse commit hash from comment id: parse-commit - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const commentBody = context.payload.comment.body; @@ -143,7 +143,7 @@ jobs: if: | (steps.pr-author.outputs.has-access != 'true' || steps.pr-author-org.outputs.is-member != 'true') && steps.parse-commit.outputs.has-commit != 'true' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | await github.rest.issues.createComment({ @@ -156,7 +156,7 @@ jobs: - name: Get PR branch details id: pr-details - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const { data: pr } = await github.rest.pulls.get({ @@ -173,7 +173,7 @@ jobs: - name: Determine commit SHA to use id: commit-sha - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const parseCommitOutput = '${{ steps.parse-commit.outputs.has-commit }}'; @@ -195,7 +195,7 @@ jobs: - name: Validate commit exists in PR if: steps.parse-commit.outputs.has-commit == 'true' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const commitSha = '${{ steps.commit-sha.outputs.sha }}'; @@ -228,8 +228,9 @@ jobs: - name: Determine validation type and pipeline ID id: validation-type + env: + COMMENT_BODY: ${{ github.event.comment.body }} run: | - COMMENT_BODY="${{ github.event.comment.body }}" if echo "$COMMENT_BODY" | grep -q "/dart"; then echo "type=dart" >> $GITHUB_OUTPUT echo "pipeline-id=15324" >> $GITHUB_OUTPUT @@ -240,9 +241,9 @@ jobs: - name: Determine target branch for pipeline id: target-branch + env: + BASE_BRANCH: ${{ steps.pr-details.outputs.base }} run: | - BASE_BRANCH="${{ steps.pr-details.outputs.base }}" - # Determine pipeline version based on target branch # Use the target branch for release/* and feature/*, otherwise use main if [ "$BASE_BRANCH" = "main" ]; then @@ -259,18 +260,21 @@ jobs: - name: Trigger Pipeline id: trigger-pipeline + env: + VALIDATION_TYPE: ${{ steps.validation-type.outputs.type }} + PIPELINE_ID: ${{ steps.validation-type.outputs.pipeline-id }} + PIPELINE_VERSION: ${{ steps.target-branch.outputs.pipeline-version }} + HAS_COMMIT: ${{ steps.parse-commit.outputs.has-commit }} + PR_NUMBER: ${{ github.event.issue.number }} + COMMIT_SHA: ${{ steps.commit-sha.outputs.sha }} run: | # Get Azure DevOps access token AZDO_TOKEN=$(az account get-access-token --resource 499b84ac-1321-427f-aa17-267ca6975798 --query accessToken -o tsv) - VALIDATION_TYPE="${{ steps.validation-type.outputs.type }}" - PIPELINE_ID="${{ steps.validation-type.outputs.pipeline-id }}" - PIPELINE_VERSION="${{ steps.target-branch.outputs.pipeline-version }}" DEVDIV_ORG="devdiv" DEVDIV_PROJECT="DevDiv" # Determine if commit was explicitly provided (EnforceLatestCommit should be false if commit was provided) - HAS_COMMIT="${{ steps.parse-commit.outputs.has-commit }}" if [ "$HAS_COMMIT" = "true" ]; then ENFORCE_LATEST="false" else @@ -294,8 +298,8 @@ jobs: } }, "templateParameters": { - "prNumber": "${{ github.event.issue.number }}", - "sha": "${{ steps.commit-sha.outputs.sha }}", + "prNumber": "$PR_NUMBER", + "sha": "$COMMIT_SHA", "EnforceLatestCommit": "$ENFORCE_LATEST" } } @@ -313,8 +317,8 @@ jobs: } }, "templateParameters": { - "PRNumber": ${{ github.event.issue.number }}, - "CommitSHA": "${{ steps.commit-sha.outputs.sha }}", + "PRNumber": $PR_NUMBER, + "CommitSHA": "$COMMIT_SHA", "EnforceLatestCommit": "$ENFORCE_LATEST" } } @@ -349,7 +353,7 @@ jobs: - name: Comment pipeline link if: success() - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | const validationType = '${{ steps.validation-type.outputs.type }}'; @@ -370,7 +374,7 @@ jobs: steps.pr-author.outcome != 'failure' && steps.pr-author-org.outcome != 'failure' && steps.parse-commit.outcome != 'failure' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: script: | await github.rest.issues.createComment({ diff --git a/Compilers.code-workspace b/Compilers.code-workspace new file mode 100644 index 000000000000..b4c836a8233c --- /dev/null +++ b/Compilers.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "dotnet.defaultSolution": "Compilers.slnf" + } +} diff --git a/Directory.Build.targets b/Directory.Build.targets index 1eaf13d9d361..95331b745af0 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -3,4 +3,5 @@ + diff --git a/Ide.code-workspace b/Ide.code-workspace new file mode 100644 index 000000000000..9f6e79b6e46d --- /dev/null +++ b/Ide.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "dotnet.defaultSolution": "Ide.slnf" + } +} diff --git a/Ide.slnf b/Ide.slnf index 4b59871d308c..ba77f258298d 100644 --- a/Ide.slnf +++ b/Ide.slnf @@ -116,8 +116,6 @@ "src\\Features\\ExternalAccess\\OmniSharp.CSharp\\Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.CSharp.csproj", "src\\Features\\ExternalAccess\\OmniSharpTest\\Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.UnitTests.csproj", "src\\Features\\ExternalAccess\\OmniSharp\\Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.csproj", - "src\\Features\\Lsif\\GeneratorTest\\Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests.vbproj", - "src\\Features\\Lsif\\Generator\\Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.csproj", "src\\Features\\TestUtilities\\Microsoft.CodeAnalysis.Features.Test.Utilities.csproj", "src\\Features\\Test\\Microsoft.CodeAnalysis.Features.UnitTests.csproj", "src\\Features\\VisualBasicTest\\Microsoft.CodeAnalysis.VisualBasic.Features.UnitTests.vbproj", diff --git a/Roslyn.code-workspace b/Roslyn.code-workspace new file mode 100644 index 000000000000..16294a1e3504 --- /dev/null +++ b/Roslyn.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "dotnet.defaultSolution": "Roslyn.slnx" + } +} diff --git a/azure-pipelines-integration-dartlab.yml b/azure-pipelines-integration-dartlab.yml index 1405348ee994..75b89b8b4c81 100644 --- a/azure-pipelines-integration-dartlab.yml +++ b/azure-pipelines-integration-dartlab.yml @@ -17,7 +17,7 @@ resources: name: DartLab.Templates ref: main - repository: RoslynMirror - endpoint: dnceng/internal + endpoint: dnceng-internal-code-access type: git name: internal/dotnet-roslyn ref: $(Build.SourceBranch) diff --git a/azure-pipelines-integration-lsp.yml b/azure-pipelines-integration-lsp.yml index 8723c16fcc8b..5adb09a4fdf6 100644 --- a/azure-pipelines-integration-lsp.yml +++ b/azure-pipelines-integration-lsp.yml @@ -53,12 +53,9 @@ parameters: - name: queueName displayName: Queue Name type: string - default: windows.vs2022preview.amd64.open + default: windows.vs2022.amd64.open values: - windows.vs2022.amd64.open - - windows.vs2022.scout.amd64.open - - windows.vs2022preview.amd64.open - - windows.vs2022preview.scout.amd64.open - name: timeout displayName: Timeout in Minutes type: number diff --git a/azure-pipelines-integration-scouting.yml b/azure-pipelines-integration-scouting.yml index 3915921ed5fd..b7fec92f7e85 100644 --- a/azure-pipelines-integration-scouting.yml +++ b/azure-pipelines-integration-scouting.yml @@ -29,12 +29,17 @@ parameters: - name: queueName displayName: Queue Name type: string - default: windows.vs2022preview.scout.amd64.open + default: windows.vs2022.amd64.open values: - windows.vs2022.amd64.open - - windows.vs2022.scout.amd64.open - - windows.vs2022preview.amd64.open - - windows.vs2022preview.scout.amd64.open + - windows.vs2026preview.scout.amd64.open +- name: buildQueueName + displayName: Build Queue Name + type: string + default: windows.vs2026preview.scout.amd64.open + values: + - windows.vs2022.amd64.open + - windows.vs2026preview.scout.amd64.open - name: timeout displayName: Timeout in Minutes type: number @@ -45,6 +50,7 @@ stages: parameters: poolName: ${{ parameters.poolName }} queueName: ${{ parameters.queueName }} + buildQueueName: ${{ parameters.buildQueueName }} timeout: ${{ parameters.timeout }} configuration: Debug testRuns: @@ -59,6 +65,7 @@ stages: parameters: poolName: ${{ parameters.poolName }} queueName: ${{ parameters.queueName }} + buildQueueName: ${{ parameters.buildQueueName }} timeout: ${{ parameters.timeout }} configuration: Release testRuns: diff --git a/azure-pipelines-integration.yml b/azure-pipelines-integration.yml index af89d7fef21c..c831eb2c2d38 100644 --- a/azure-pipelines-integration.yml +++ b/azure-pipelines-integration.yml @@ -70,12 +70,17 @@ parameters: - name: queueName displayName: Queue Name type: string - default: windows.vs2022preview.scout.amd64.open + default: windows.vs2022.amd64.open values: - windows.vs2022.amd64.open - - windows.vs2022.scout.amd64.open - - windows.vs2022preview.amd64.open - - windows.vs2022preview.scout.amd64.open + - windows.vs2026preview.scout.amd64.open +- name: buildQueueName + displayName: Build Queue Name + type: string + default: windows.vs2026preview.scout.amd64.open + values: + - windows.vs2022.amd64.open + - windows.vs2026preview.scout.amd64.open - name: timeout displayName: Timeout in Minutes type: number @@ -86,6 +91,7 @@ stages: parameters: poolName: ${{ parameters.poolName }} queueName: ${{ parameters.queueName }} + buildQueueName: ${{ parameters.buildQueueName }} timeout: ${{ parameters.timeout }} configuration: Debug testRuns: @@ -101,6 +107,7 @@ stages: parameters: poolName: ${{ parameters.poolName }} queueName: ${{ parameters.queueName }} + buildQueueName: ${{ parameters.buildQueueName }} timeout: ${{ parameters.timeout }} configuration: Release testRuns: diff --git a/azure-pipelines-official.yml b/azure-pipelines-official.yml index 32440b5c06e1..42416c7b1efa 100644 --- a/azure-pipelines-official.yml +++ b/azure-pipelines-official.yml @@ -7,6 +7,8 @@ trigger: - release/dev16.*-vs-deps - release/dev17.* - release/dev18.* + - release/insiders + - release/stable - features/lsp_tools_host - features/runtime-async exclude: @@ -45,6 +47,10 @@ parameters: type: boolean default: true +- name: PMEOverride + type: boolean + default: false + # An optional VS commit SHA that will be automatically cherry-picked # to the insertion PR created by this build. - name: VisualStudioCherryPickSHA @@ -116,6 +122,8 @@ variables: extends: template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates parameters: + settings: + networkIsolationPolicy: Permissive, CFSClean, CFSClean2 featureFlags: autoBaseline: true autoEnablePREfastWithNewRuleset: false @@ -136,7 +144,7 @@ extends: configFile: '$(Build.SourcesDirectory)/eng/TSAConfig.gdntsa' pool: name: $(DncEngInternalBuildPool) - image: windows.vs2022preview.scout.amd64 + image: windows.vs2026.amd64 os: windows customBuildTags: - ES365AIMigrationTooling @@ -300,7 +308,7 @@ extends: zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json # Set ConnectedPMEServiceName if the build is a CI build, otherwise it is not needed - ${{ if or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'Schedule')) }}: + ${{ if or(eq(variables['Build.Reason'], 'IndividualCI'), eq(variables['Build.Reason'], 'Schedule'), parameters.PMEOverride) }}: ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca condition: and(succeeded(), in('${{ parameters.SignType }}', 'test', 'real')) @@ -403,10 +411,8 @@ extends: displayName: Get Branch Name - template: /eng/pipelines/insert.yml@self parameters: - buildUserName: "dn-bot@microsoft.com" - buildPassword: $(dn-bot-devdiv-build-e-code-full-release-e-packaging-r) - componentUserName: "dn-bot@microsoft.com" - componentPassword: $(dn-bot-dnceng-build-e-code-full-release-e-packaging-r) + devDivAzdoToken: $(dn-bot-devdiv-build-e-code-full-release-e-packaging-r) + dncEngAzdoToken: $(dn-bot-dnceng-build-e-code-full-release-e-packaging-r) componentBuildProjectName: internal sourceBranch: "$(ComponentBranchName)" publishDataURI: "https://dev.azure.com/dnceng/internal/_apis/git/repositories/dotnet-roslyn/items?path=eng/config/PublishData.json&version=$(ComponentBranchName)&api-version=6.0" diff --git a/azure-pipelines-pr-validation.yml b/azure-pipelines-pr-validation.yml index 5ca25477f2f8..7ea851ec2ca8 100644 --- a/azure-pipelines-pr-validation.yml +++ b/azure-pipelines-pr-validation.yml @@ -19,9 +19,6 @@ parameters: - name: VisualStudioBranchName type: string default: default -- name: OptionalTitlePrefix - type: string - default: '[PR Validation]' - name: VisualStudioCherryPickSHA type: string default: '(default)' @@ -53,7 +50,7 @@ extends: sdl: enableAllTools: false pool: - name: VSEngSS-MicroBuild2022-1ES + name: VSEng-MicroBuildVSStable demands: - msbuild - visualstudio @@ -151,7 +148,7 @@ extends: steps: - pwsh: Set-MpPreference -DisableRealtimeMonitoring $true displayName: Disable Real-time Monitoring - + - task: Powershell@2 name: SetOriginalBuildNumber displayName: Setting OriginalBuildNumber variable @@ -203,6 +200,12 @@ extends: Write-Host "##vso[task.setvariable variable=BuildNumber;isoutput=true;isreadonly=true]$buildNumberName" Write-Host "##vso[build.updatebuildnumber]$buildNumberName" + $email = git log -1 --pretty=format:"%ae" ${{ parameters.CommitSHA }} + $parts = $email -split '@', 2 + $localPart = $parts[0] + $prTitlePrefix = "[PR #${{ parameters.PRNumber }} $localPart]" + Write-Host "##vso[task.setvariable variable=PRTitlePrefix;isoutput=true;isreadonly=true]$prTitlePrefix" + - powershell: Write-Host "##vso[task.setvariable variable=VisualStudio.DropName]Products/$(System.TeamProject)/$(Build.Repository.Name)/$(SourceBranchName)/$(OriginalBuildNumber)" displayName: Setting VisualStudio.DropName variable @@ -326,6 +329,7 @@ extends: jobs: - job: insert variables: + PRTitlePrefix: $[stageDependencies.build.PRValidationBuild.outputs['FancyBuild.PRTitlePrefix']] FancyBuildNumber: $[stageDependencies.build.PRValidationBuild.outputs['FancyBuild.BuildNumber']] OriginalBuildNumber: $[stageDependencies.build.PRValidationBuild.outputs['SetOriginalBuildNumber.OutputOriginalBuildNumber']] displayName: Insert to VS @@ -350,13 +354,12 @@ extends: createDraftPR: true autoComplete: false insertToolset: ${{ parameters.InsertToolset }} - buildUserName: "dn-bot@microsoft.com" - buildPassword: $(dn-bot-devdiv-build-e-code-full-release-e-packaging-r) - componentUserName: "dn-bot@microsoft.com" - componentPassword: $(dn-bot-dnceng-build-e-code-full-release-e-packaging-r) + devDivAzdoToken: $(dn-bot-devdiv-build-e-code-full-release-e-packaging-r) + dncEngAzdoToken: $(dn-bot-dnceng-build-e-code-full-release-e-packaging-r) vsBranchName: ${{ parameters.VisualStudioBranchName }} - titlePrefix: ${{ parameters.OptionalTitlePrefix }} + titlePrefix: $(PRTitlePrefix) sourceBranch: $(ComponentBranchName) + componentBuildProjectName: $(System.TeamProject) publishDataURI: "https://raw.githubusercontent.com/dotnet/roslyn/$(ComponentBranchName)/eng/config/PublishData.json" queueSpeedometerValidation: true dropPath: '$(Pipeline.Workspace)\VSSetup' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e63dc7c5b4c9..a648aedd806b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -56,14 +56,6 @@ variables: ${{ else }}: value: NetCore1ESPool-Internal - - name: Vs2022PreviewQueueName - # As of 2025-06-12, the non-scouting queue does not have a new enough of an MSBuild to build Roslyn; use the scouting - # queue which is on 17.14. This likely can be removed by 2025-06-18 when the upgrade to 17.14 Preview 4 is scheduled to complete. - ${{ if eq(variables['System.TeamProject'], 'public') }}: - value: windows.vs2022preview.scout.amd64.open - ${{ else }}: - value: windows.vs2022preview.scout.amd64 - - name: Vs2022RegularQueueName ${{ if eq(variables['System.TeamProject'], 'public') }}: value: windows.vs2022.amd64.open @@ -96,9 +88,9 @@ variables: - name: HelixWindowsVsQueueName ${{ if eq(variables['System.TeamProject'], 'public') }}: - value: windows.amd64.vs2022.pre.open + value: windows.amd64.vs2026.pre.scout.open ${{ else }}: - value: windows.amd64.vs2022.pre + value: windows.amd64.vs2026.pre.scout - name: HelixWindowsSpanishQueueName ${{ if eq(variables['System.TeamProject'], 'public') }}: @@ -267,16 +259,17 @@ stages: helixApiAccessToken: $(HelixApiAccessToken) poolParameters: ${{ parameters.windowsPool }} - - template: eng/pipelines/test-windows-job.yml - parameters: - testRunName: 'Test Windows Desktop Spanish Release 64' - jobName: Test_Windows_Desktop_Spanish_Release_64 - testArtifactName: Transport_Artifacts_Windows_Release - configuration: Release - testArguments: -testDesktop -testArch x64 - helixQueueName: $(HelixWindowsSpanishQueueName) - helixApiAccessToken: $(HelixApiAccessToken) - poolParameters: ${{ parameters.windowsPool }} + - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: + - template: eng/pipelines/test-windows-job.yml + parameters: + testRunName: 'Test Windows Desktop Spanish Release 64' + jobName: Test_Windows_Desktop_Spanish_Release_64 + testArtifactName: Transport_Artifacts_Windows_Release + configuration: Release + testArguments: -testDesktop -testArch x64 + helixQueueName: $(HelixWindowsSpanishQueueName) + helixApiAccessToken: $(HelixApiAccessToken) + poolParameters: ${{ parameters.windowsPool }} - stage: Windows_Debug_CoreClr dependsOn: Windows_Debug_Build @@ -386,15 +379,15 @@ stages: helixApiAccessToken: $(HelixApiAccessToken) poolParameters: ${{ parameters.ubuntuPool }} - - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - - template: eng/pipelines/test-unix-job-single-machine.yml - parameters: - testRunName: 'Test Linux Debug Single Machine' - jobName: Test_Linux_Debug_Single_Machine - testArtifactName: Transport_Artifacts_Unix_Debug - configuration: Debug - testArguments: --testCoreClr - poolParameters: ${{ parameters.ubuntuPool }} + - template: eng/pipelines/test-unix-job-single-machine.yml + parameters: + testRunName: 'Test Linux Debug Spanish Single Machine' + jobName: Test_Linux_Debug_Spanish_Single_Machine + testArtifactName: Transport_Artifacts_Unix_Debug + configuration: Debug + testArguments: --testCoreClr + poolParameters: ${{ parameters.ubuntuPool }} + locale: es_ES.UTF-8 - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - stage: MacOS_Debug_CoreClr @@ -410,7 +403,7 @@ stages: jobName: Test_macOS_Debug testArtifactName: Transport_Artifacts_macOS_Debug configuration: Debug - testArguments: --testCoreClr + testArguments: --testCoreClr helixQueueName: $(HelixMacOsQueueName) helixApiAccessToken: $(HelixApiAccessToken) poolParameters: ${{ parameters.ubuntuPool }} @@ -456,7 +449,7 @@ stages: steps: - template: eng/pipelines/checkout-windows-task.yml - - powershell: eng/build.ps1 -configuration Release -prepareMachine -ci -restore -binaryLogName Restore.binlog + - powershell: eng/build.ps1 -configuration Release -prepareMachine -ci -restore -binaryLogName Restore.binlog displayName: Restore # We additionally restore during the build because the Microsoft.DotNet.Build.Tasks.Feed package only restores when we pass `-publish`. See https://github.com/dotnet/arcade/blob/37ccfd66358af6a37a0ec385ec31d1d71bdd8723/src/Microsoft.DotNet.Arcade.Sdk/tools/Tools.proj#L61-L66 @@ -499,7 +492,7 @@ stages: custom: 'run' arguments: '--file $(Build.SourcesDirectory)/eng/ensure-sources-synced.cs' - - powershell: eng/validate-code-formatting.ps1 -ci -rootDirectory $(Build.SourcesDirectory)\src -includeDirectories Compilers\CSharp\Portable\Generated\, Compilers\VisualBasic\Portable\Generated\, ExpressionEvaluator\VisualBasic\Source\ResultProvider\Generated\ + - powershell: eng/validate-code-formatting.ps1 -ci -rootDirectory $(Build.SourcesDirectory)\src -includeDirectories Compilers\CSharp\Portable\Generated\, Compilers\VisualBasic\Portable\Generated\, ExpressionEvaluator\VisualBasic\Source\ResultProvider\Generated\ displayName: Validate Generated Syntax Files condition: or(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['compilerChange'], 'true')) diff --git a/docs/Glossary - C# Language Server Ecosystem.md b/docs/Glossary - C# Language Server Ecosystem.md new file mode 100644 index 000000000000..f5f245366395 --- /dev/null +++ b/docs/Glossary - C# Language Server Ecosystem.md @@ -0,0 +1,118 @@ +# Glossary: C# Language Server Ecosystem + +A reference for common terms, acronyms, and components in the C# language server ecosystem. + +--- + +## Protocol & Standards + +| Term | Definition | +|------|------------| +| **LSP** (Language Server Protocol) | The JSON-RPC based protocol specification that defines communication between an editor (client) and a language server. Defines the types, methods, and lifecycle for features like completion, diagnostics, go-to-definition, etc. Spec: https://microsoft.github.io/language-server-protocol/ | +| **JSON-RPC** | The remote procedure call protocol used as the transport layer for LSP. Messages are encoded as JSON and sent over stdio, named pipes, or other transports. | +| **Custom LSP Messages** | LSP allows servers and clients to define custom request and notification methods beyond the standard specification. Roslyn uses custom messages (prefixed with `roslyn/`) for features not covered by the standard protocol, such as code action resolve, project diagnostics, and source generator support. | +| **VS LSP Extensions** | Visual Studio extends LSP with additional protocol methods and types (prefixed with `vs/` or `_vs_`) to support richer IDE features that the standard LSP spec does not cover, such as document highlighting, project context, icon mappings, and rich diagnostics. These extensions are available when the Roslyn language server runs inside Visual Studio and can be negotiated via capabilities. | + +## Language Servers + +| Term | Definition | +|------|------------| +| **Roslyn Language Server (RLS)** | The standalone Roslyn-powered language server executable (`roslyn-language-server`). Built from `src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/`. Provides C# (and VB) language features over LSP. Used both by the C# extension and as a standalone tool. Also referred to as "Roslyn LS." | +| **OmniSharp (O#)** | The community run open-source C# language server that predated the Roslyn Language Server. Can be run as a standalone server process (not limited to VS Code) and supports both the standard LSP protocol and its own custom OmniSharp protocol. Previously powered the C# VS Code extension. Repo: [OmniSharp/omnisharp-roslyn](https://github.com/OmniSharp/omnisharp-roslyn). | + +## Roslyn LSP Architecture + +| Term | Definition | +|------|------------| +| **Microsoft.CodeAnalysis.LanguageServer** | The project hosting the standalone language server executable (`roslyn-language-server`). References LanguageServer.Protocol and packages it into a runnable .NET tool that communicates over stdio or named pipes. Used by the C# VS Code extension (which launches it as a child process) and available as a CLI tool via `dotnet tool install`. Includes JPS (but can be deactivated). Located in `src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/`. | +| **Microsoft.CodeAnalysis.LanguageServer.Protocol** | The shared LSP implementation library used by all Roslyn LSP scenarios. Contains LSP request handlers, protocol type definitions, workspace integration, and feature wiring. This project is consumed by both the standalone language server executable and the Visual Studio in-process LSP server, providing a single implementation of Roslyn's language features over LSP. Located in `src/LanguageServer/Protocol/`. | +| **CLaSP** (Common Language Server Protocol Framework) | A framework built by Roslyn for creating LSP server implementations. Provides base classes (`AbstractLanguageServer`, `IRequestHandler`, `IRequestContextFactory`, etc.) and request queue management. Located in `src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/`. | +| **Roslyn LSP Protocol Types** | The C# type definitions for LSP protocol messages, shared with Razor and XAML. Located in `src/LanguageServer/Protocol/Protocol/`. | + +## Project Systems + +| Term | Definition | +|------|------------| +| **CPS** (Common Project System) / **.NET Project System** | The project system used by C# Dev Kit and Visual Studio. Provides full MSBuild-based project evaluation and design-time builds. Shipped and hosted by the C# Dev Kit extension in VSCode. Communicates cross process with RLS in the C# extension | +| **JPS** (Jason Project System) | A lightweight project system used by Roslyn Language Server when not connected to C# Dev Kit (aka C# extension only, or CLI scenarios). Provides project discovery and loading. | + +## VSCode Extensions + +| Term | Definition | +|------|------------| +| **C# Extension (standalone)** | The [C# for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) extension running without C# Dev Kit. Uses the Roslyn Language Server (RLS) for language features and the Jason Project System (JPS) for project loading. | +| **C# Dev Kit (C#DK)** | The [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) VS Code extension. Hosts independent processes that add solution management, test explorer, and richer project system support on top of the C# extension. | +| **C# + C# Dev Kit** | The combination of the C# extension and C# Dev Kit extension installed together in VS Code. In this scenario, the Roslyn Language Server is connected to C# Dev Kit's ServiceHub server via a cross process bridge. JPS is deactivated, and CPS provides project information to the Roslyn Language Server via this bridge. | + +## Architecture Diagram + +```mermaid +graph TB + subgraph VSCode["VS Code"] + subgraph CSharpExt["C# Extension"] + ExtClient["LSP Client"] + end + subgraph CSharpDK["C# Dev Kit Extension"] + DKClient["Extension Host"] + end + end + + subgraph RLS["Roslyn Language Server
(roslyn-language-server)"] + RLSCore["Roslyn Language Server"] + Protocol["LanguageServer.Protocol
(LSP handlers, features)"] + JPS["JPS
(JSON Project System)
active only without C#DK"] + CLaSP["CLaSP Framework"] + RLSCore --> Protocol + RLSCore --> JPS + Protocol --> CLaSP + end + + subgraph ServiceHubProcesses["C# Dev Kit Server Processes"] + ServiceHub["ServiceHub /
Service Broker"] + CPS["CPS / .NET Project System"] + TestHost["Test Host"] + DebugHost["Debug Host"] + end + + ExtClient -- "LSP
(stdio / named pipe)" --> RLS + DKClient -- "Service Broker" --> ServiceHub + RLS <-- "Cross-process bridge
(project info, solution state)" --> ServiceHub + CPS --> ServiceHub + + style VSCode fill:#2b2b2b,stroke:#007acc,color:#fff + style CSharpExt fill:#1e1e1e,stroke:#68217a,color:#fff + style CSharpDK fill:#1e1e1e,stroke:#68217a,color:#fff + style ServiceHubProcesses fill:#1a2a3a,stroke:#4e9ac9,color:#fff +``` + +> **C# Extension standalone** (without C# Dev Kit): Only the right side is active — the C# extension launches RLS, which uses JPS for project loading. The C# Dev Kit extension and its ServiceHub processes are not present. +> +> **C# + C# Dev Kit**: Both extensions are active. C# Dev Kit launches ServiceHub processes hosting CPS and other services. RLS connects to ServiceHub via a cross-process bridge, JPS is deactivated, and CPS provides project information to RLS. + +## Standalone Tool Architecture + +```mermaid +graph TB + subgraph Editor["Copilot CLI"] + Client["LSP Client"] + end + + subgraph RLSProcess["roslyn-language-server Process"] + RLS["Roslyn Language Server"] + Protocol["LanguageServer.Protocol
(LSP handlers, features)"] + JPS["JPS
(JSON Project System)"] + CLaSP["CLaSP Framework"] + Protocol --> CLaSP + RLS --> Protocol + RLS --> JPS + end + + Client -- "LSP
(stdio / named pipe)" --> RLS + JPS -- "discovers & loads" --> Projects[".csproj / .sln files"] + + style Editor fill:#2b2b2b,stroke:#007acc,color:#fff + style RLSProcess fill:#1a3a1a,stroke:#4ec94e,color:#fff +``` + +> **Standalone usage**: The `roslyn-language-server` can be installed as a .NET tool (`dotnet tool install --global roslyn-language-server`). It runs as a single process, uses JPS for project discovery, and communicates over stdio or named pipes. + diff --git a/docs/Language Feature Status.md b/docs/Language Feature Status.md index 14a77824365f..25be885d050c 100644 --- a/docs/Language Feature Status.md +++ b/docs/Language Feature Status.md @@ -17,10 +17,10 @@ When a feature is merged to the `main` branch, its entry should be moved to the | Feature | Branch | State | Developer | Reviewer | IDE Buddy | LDM Champ | | ------- | ------ | ----- | --------- | -------- | --------- | --------- | | [Dictionary expressions](https://github.com/dotnet/csharplang/issues/8659) | [dictionary-expressions](https://github.com/dotnet/roslyn/tree/features/dictionary-expressions) | [In progress](https://github.com/dotnet/roslyn/issues/81860) | [333fred](https://github.com/333fred) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi), [jcouv](https://github.com/jcouv) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | -| [Unions](https://github.com/dotnet/csharplang/issues/9662) | [Unions](https://github.com/dotnet/roslyn/tree/features/Unions) | [In Progress](https://github.com/dotnet/roslyn/issues/81074) | [AlekseyTs](https://github.com/AlekseyTs) | [RikkiGibson](https://github.com/RikkiGibson), [333fred](https://github.com/333fred) | TBD | [MadsTorgersen](https://github.com/MadsTorgersen) | +| [Unions](https://github.com/dotnet/csharplang/issues/9662) | [Unions](https://github.com/dotnet/roslyn/tree/features/Unions) | [In Progress](https://github.com/dotnet/roslyn/issues/81074) | [AlekseyTs](https://github.com/AlekseyTs) | [RikkiGibson](https://github.com/RikkiGibson), [jjonescz](https://github.com/jjonescz) | TBD | [MadsTorgersen](https://github.com/MadsTorgersen) | | [Closed class hierarchies](https://github.com/dotnet/csharplang/issues/9499) | [closed-class](https://github.com/dotnet/roslyn/tree/features/closed-class) | [In progress](https://github.com/dotnet/roslyn/issues/81039) | [RikkiGibson](https://github.com/RikkiGibson) | [AlekseyTs](https://github.com/AlekseyTs), [jjonescz](https://github.com/jjonescz) | TBD | [mattwar](https://github.com/mattwar) | -| [Unsafe evolution](https://github.com/dotnet/csharplang/issues/9704) | [UnsafeEvolution](https://github.com/dotnet/roslyn/tree/features/UnsafeEvolution) | [In progress](https://github.com/dotnet/roslyn/issues/81207) | [jjonescz](https://github.com/jjonescz) | [333fred](https://github.com/333fred), [jcouv](https://github.com/jcouv) | TBD | [agocke](https://github.com/agocke) | | [Extension indexers](https://github.com/dotnet/csharplang/issues/9856) | [extensions](https://github.com/dotnet/roslyn/tree/features/extensions) | [In progress](https://github.com/dotnet/roslyn/issues/81505) | [jcouv](https://github.com/jcouv) | [AlekseyTs](https://github.com/AlekseyTs), [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [jcouv](https://github.com/jcouv) | +| [Unsafe evolution](https://github.com/dotnet/csharplang/issues/9704) | [UnsafeEvolution](https://github.com/dotnet/roslyn/tree/features/UnsafeEvolution) | [Merged as preview feature into .NET 11p2 and VS 18.6](https://github.com/dotnet/roslyn/issues/81207) | [jjonescz](https://github.com/jjonescz) | [333fred](https://github.com/333fred), [jcouv](https://github.com/jcouv) | TBD | [agocke](https://github.com/agocke) | | [ExtendedLayoutAttribute](https://github.com/dotnet/runtime/issues/100896) | main | [Merged into 18.3](https://github.com/dotnet/roslyn/pull/78741) | [jkoritzinsky](https://github.com/jkoritzinsky) | [333fred](https://github.com/333fred), [jcouv](https://github.com/jcouv) | | | | Runtime Async | [runtime-async-streams](https://github.com/dotnet/roslyn/tree/features/runtime-async-streams) | [Main feature merged into main in preview](https://github.com/dotnet/roslyn/issues/75960) | [333fred](https://github.com/333fred) | [jcouv](https://github.com/jcouv), [RikkiGibson](https://github.com/RikkiGibson) | | | | [Collection expression arguments](https://github.com/dotnet/csharplang/issues/8887) | [collection-expression-arguments](https://github.com/dotnet/roslyn/tree/features/collection-expression-arguments) | [Merged into .NET 11 p1 and VS 18.4](https://github.com/dotnet/roslyn/issues/80613) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [333fred](https://github.com/333fred), [jcouv](https://github.com/jcouv) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | diff --git a/docs/Layering.md b/docs/Layering.md new file mode 100644 index 000000000000..6f856884acaf --- /dev/null +++ b/docs/Layering.md @@ -0,0 +1,138 @@ +# Roslyn Architecture - Project Layering + +Arrows point from a project **up to** its dependencies. + +```mermaid +graph BT + + subgraph OSS["OSS-only dependencies"] + subgraph Layer0[" "] + subgraph L0["Compilers"] + Microsoft_CodeAnalysis["Microsoft.CodeAnalysis"] + end + end + + subgraph Layer1[" "] + subgraph L1a["Workspaces"] + Microsoft_CodeAnalysis_Workspaces["Workspaces"] + Microsoft_CodeAnalysis_Workspaces_BuildHost["Workspaces.BuildHost"] + Microsoft_CodeAnalysis_Workspaces_MSBuild["Workspaces.MSBuild"] + Microsoft_CodeAnalysis_Workspaces_BuildHost_Contracts["Workspaces.BuildHost.Contracts"] + end + + subgraph L1b["CodeStyle"] + Microsoft_CodeAnalysis_CodeStyle["CodeStyle"] + Microsoft_CodeAnalysis_CodeStyle_Fixes["CodeStyle.Fixes"] + end + + subgraph L1c["Scripting"] + Microsoft_CodeAnalysis_Scripting["Scripting"] + Microsoft_CodeAnalysis_InteractiveHost["InteractiveHost"] + end + end + + subgraph Layer3[" "] + subgraph L3a["Features"] + Microsoft_CodeAnalysis_Features["Features"] + Microsoft_CodeAnalysis_LanguageServer_Protocol["LanguageServer.Protocol"] + end + end + + subgraph L6b["Language Server"] + Microsoft_CodeAnalysis_LanguageServer["LanguageServer"] + end + + subgraph L6e["Compiler Server"] + VBCSCompiler["VBCSCompiler"] + end + end + + subgraph L3b["Features"] + Microsoft_CodeAnalysis_Remote_Workspaces["Remote.Workspaces"] + end + + subgraph Layer4[" "] + subgraph L4["Editor Features"] + Microsoft_CodeAnalysis_EditorFeatures_Text["EditorFeatures.Text"] + Microsoft_CodeAnalysis_EditorFeatures["EditorFeatures"] + end + end + + subgraph Layer5["Shared Brokered Services"] + Brokered_Services_General["General services"] + Brokered_Services_Client["Client services"] + end + + subgraph Layer6[" "] + subgraph L6a["Visual Studio"] + Microsoft_VisualStudio_LanguageServices["VS.LanguageServices"] + Microsoft_VisualStudio_LanguageServices_Implementation["VS.Implementation"] + end + + subgraph L6c["Visual Studio OOP"] + Microsoft_CodeAnalysis_Remote_ServiceHub["Remote.ServiceHub"] + end + + subgraph L6d["Interactive Host Executable"] + InteractiveHost_Exe["InteractiveHost(32|64)"] + end + + subgraph L6f["C# DevKit"] + Microsoft_VisualStudio_LanguageServices_DevKit["VS.LanguageServices.DevKit"] + end + end + + Microsoft_CodeAnalysis_CodeStyle --> Microsoft_CodeAnalysis + Microsoft_CodeAnalysis_CodeStyle_Fixes --> Microsoft_CodeAnalysis_CodeStyle + Microsoft_CodeAnalysis_CodeStyle_Fixes --> Microsoft_CodeAnalysis_Workspaces + Microsoft_CodeAnalysis_EditorFeatures --> Microsoft_CodeAnalysis_EditorFeatures_Text + Microsoft_CodeAnalysis_EditorFeatures --> Microsoft_CodeAnalysis_InteractiveHost + Microsoft_CodeAnalysis_EditorFeatures --> Microsoft_CodeAnalysis_LanguageServer_Protocol + Microsoft_CodeAnalysis_EditorFeatures --> Microsoft_CodeAnalysis_Remote_Workspaces + Microsoft_CodeAnalysis_EditorFeatures_Text --> Microsoft_CodeAnalysis_Workspaces + Microsoft_CodeAnalysis_Features --> Microsoft_CodeAnalysis_Scripting + Microsoft_CodeAnalysis_Features --> Microsoft_CodeAnalysis_Workspaces + Microsoft_CodeAnalysis_InteractiveHost --> Microsoft_CodeAnalysis_Scripting + Microsoft_CodeAnalysis_LanguageServer --> Microsoft_CodeAnalysis_LanguageServer_Protocol + Microsoft_CodeAnalysis_LanguageServer --> Microsoft_CodeAnalysis_Workspaces_MSBuild + Microsoft_CodeAnalysis_LanguageServer_Protocol --> Microsoft_CodeAnalysis_Features + Microsoft_CodeAnalysis_Remote_Workspaces --> Microsoft_CodeAnalysis_Features + Microsoft_CodeAnalysis_Remote_ServiceHub --> Microsoft_CodeAnalysis_Remote_Workspaces + InteractiveHost_Exe --> Microsoft_CodeAnalysis_InteractiveHost + VBCSCompiler --> Microsoft_CodeAnalysis + Microsoft_CodeAnalysis_Scripting --> Microsoft_CodeAnalysis + Microsoft_CodeAnalysis_Workspaces --> Microsoft_CodeAnalysis + Microsoft_CodeAnalysis_Workspaces_MSBuild --> Microsoft_CodeAnalysis_Workspaces + Microsoft_CodeAnalysis_Workspaces_MSBuild --> Microsoft_CodeAnalysis_Workspaces_BuildHost + Microsoft_CodeAnalysis_Workspaces_MSBuild --> Microsoft_CodeAnalysis_Workspaces_BuildHost_Contracts + Microsoft_CodeAnalysis_Workspaces_BuildHost --> Microsoft_CodeAnalysis_Workspaces_BuildHost_Contracts + Microsoft_VisualStudio_LanguageServices --> Microsoft_CodeAnalysis_EditorFeatures + Microsoft_VisualStudio_LanguageServices_Implementation --> Microsoft_VisualStudio_LanguageServices + Microsoft_VisualStudio_LanguageServices_DevKit --> Microsoft_CodeAnalysis_LanguageServer + Microsoft_CodeAnalysis_Remote_ServiceHub --> Brokered_Services_General + Microsoft_VisualStudio_LanguageServices --> Brokered_Services_General + Microsoft_VisualStudio_LanguageServices_DevKit --> Brokered_Services_General + Microsoft_VisualStudio_LanguageServices --> Brokered_Services_Client + Microsoft_VisualStudio_LanguageServices_DevKit --> Brokered_Services_Client + + style OSS fill:#e0ffe0,fill-opacity:0.15,stroke:#00aa00,stroke-width:2px,stroke-dasharray:5 5 + style Layer0 fill:none,stroke:none + style Layer1 fill:none,stroke:none + style Layer3 fill:none,stroke:none + style Layer4 fill:none,stroke:none + style Layer5 fill:orange,fill-opacity:0.15,stroke:orange,stroke-width:2px + style Layer6 fill:none,stroke:none + style L0 fill:#00ffc0,fill-opacity:0.15,stroke:#00ffc0,stroke-width:2px + style L1a fill:#5b9bd5,fill-opacity:0.15,stroke:#5b9bd5,stroke-width:2px + style L1b fill:#5b9bd5,fill-opacity:0.15,stroke:#5b9bd5,stroke-width:2px + style L1c fill:#5b9bd5,fill-opacity:0.15,stroke:#5b9bd5,stroke-width:2px + style L3a fill:#ffc000,fill-opacity:0.15,stroke:#ffc000,stroke-width:2px + style L3b fill:#ffc000,fill-opacity:0.15,stroke:#ffc000,stroke-width:2px + style L4 fill:#ed7d31,fill-opacity:0.15,stroke:#ed7d31,stroke-width:2px + style L6a fill:#7030a0,fill-opacity:0.15,stroke:#7030a0,stroke-width:2px + style L6b fill:#7030a0,fill-opacity:0.15,stroke:#7030a0,stroke-width:2px + style L6c fill:#7030a0,fill-opacity:0.15,stroke:#7030a0,stroke-width:2px + style L6d fill:#7030a0,fill-opacity:0.15,stroke:#7030a0,stroke-width:2px + style L6e fill:#7030a0,fill-opacity:0.15,stroke:#7030a0,stroke-width:2px + style L6f fill:#7030a0,fill-opacity:0.15,stroke:#7030a0,stroke-width:2px +``` diff --git a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 11.md b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 11.md index 64c135eb2369..a80cc1dab0b2 100644 --- a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 11.md +++ b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 11.md @@ -153,6 +153,26 @@ void M() See also https://github.com/dotnet/roslyn/issues/80954. +## `nameof(this.)` in attributes is disallowed + +***Introduced in Visual Studio 2026 version 18.3 and .NET 10.0.200*** + +Using `this` or `base` keyword inside `nameof` in an attribute was previously unintentionally allowed in Roslyn since C# 12 +and is [now properly disallowed](https://github.com/dotnet/roslyn/pull/81628) to match the language specification. +This breaking change can be mitigated by removing `this.` and accessing the member without the qualifier. + +See also https://github.com/dotnet/roslyn/issues/82251. + +```cs +class C +{ + string P; + [System.Obsolete(nameof(this.P))] // now disallowed + [System.Obsolete(nameof(P))] // workaround + void M() { } +} +``` + ## Parsing of 'with' within a switch-expression-arm ***Introduced in Visual Studio 2026 version 18.4*** diff --git a/docs/compilers/CSharp/Warnversion Warning Waves.md b/docs/compilers/CSharp/Warnversion Warning Waves.md index 23283ebdc424..f0ce4ec58cbd 100644 --- a/docs/compilers/CSharp/Warnversion Warning Waves.md +++ b/docs/compilers/CSharp/Warnversion Warning Waves.md @@ -13,6 +13,15 @@ In a typical project, this setting is controlled by the `AnalysisLevel` property which determines the `WarningLevel` property (passed to the `Csc` task). For more information on `AnalysisLevel`, see https://devblogs.microsoft.com/dotnet/automatically-find-latent-bugs-in-your-code-with-net-5/ +## Warning level 11 + +The compiler shipped with .NET 11 (the C# 15 compiler) contains the following warnings which are reported only under `/warn:11` or higher. + +| Warning ID | Description | +|------------|-------------| +| CS9368 | [RequiresUnsafeAttribute is only valid under the updated memory safety rules](https://github.com/dotnet/csharplang/issues/9704) | +| CS9377 | [The 'unsafe' modifier does not have any effect here under the current rules](https://github.com/dotnet/csharplang/issues/9704) | + ## Warning level 10 The compiler shipped with .NET 10 (the C# 14 compiler) contains the following warnings which are reported only under `/warn:10` or higher. diff --git a/docs/compilers/Deterministic Inputs.md b/docs/compilers/Deterministic Inputs.md index c865ff6ffad0..f9108283ba5d 100644 --- a/docs/compilers/Deterministic Inputs.md +++ b/docs/compilers/Deterministic Inputs.md @@ -30,3 +30,105 @@ The following are considered inputs to the compiler for the purpose of determini - The full path of source files although `/pathmap` can be used to normalize this between compiles of the same code in different root directories. At the moment the compiler also depends on the time of day and random numbers for GUIDs, so it is not deterministic unless you specify `/deterministic`. + +## Debugging Determinism Failures + +When investigating determinism issues where the same code produces different outputs, you can use several techniques to identify the cause: + +### 1. Generate a Deterministic Key File + +Build with `/p:Features="debug-determinism"` to generate a `.key` file that documents all inputs to the compiler: + +```bash +dotnet build /p:Features="debug-determinism" +``` + +This creates an additional output file (e.g., `MyAssembly.dll.key`) in the output directory (typically in the `obj` directory for intermediate builds). The key file is a JSON document containing: +- Compiler version and runtime information +- All source file paths and content checksums +- Referenced assemblies with their MVIDs (Module Version IDs) +- Compilation options +- Parse options +- Emit options +- Analyzer and generator information + +Compare the `.key` files from two supposedly identical builds to identify which inputs differ: + +```bash +# Unix/Linux/Mac +diff build1/MyAssembly.dll.key build2/MyAssembly.dll.key + +# Windows (using fc) +fc build1\MyAssembly.dll.key build2\MyAssembly.dll.key +``` + +**Note**: If the `.key` files are identical but the output assemblies differ, this indicates a compiler determinism bug. Please report such cases to the Roslyn team with a reproduction so the issue can be investigated. + +### 2. Compare Metadata Using metadata-tools + +Install the `mdv` (MetaData Viewer) tool from the [dotnet/metadata-tools](https://github.com/dotnet/metadata-tools) repository: + +```bash +dotnet tool install mdv -g --prerelease --add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json +``` + +Generate metadata dumps for your assemblies and compare them: + +```bash +# Generate metadata output for each DLL +mdv MyAssembly1.dll > assembly1.txt +mdv MyAssembly2.dll > assembly2.txt + +# Compare the outputs +diff assembly1.txt assembly2.txt +``` + +The metadata dump shows detailed information about types, methods, fields, attributes, and other metadata in the assembly. Differences in the metadata can help identify what changed between builds. + +### 3. Compare Embedded Resources + +If your assembly contains embedded resources, verify they are identical. The recommended approach is to use ILSpy, which has an easy "Save" button to export resources from the DLL for comparison. Other tools like dnSpy or `ildasm` can also be used to extract and inspect embedded resources. + +Example using `ildasm` to disassemble and extract embedded `.res` files: + +```bash +# Disassemble and generate .res files +ildasm /out=assembly1.il MyAssembly1.dll +ildasm /out=assembly2.il MyAssembly2.dll + +# Compare the generated .res files +diff assembly1.res assembly2.res +``` + +### 4. Binary Diff of the DLL + +As a last resort, perform a hex dump comparison of the actual DLL files: + +```bash +# Unix/Linux/Mac - using xxd or hexdump +xxd MyAssembly1.dll > assembly1.hex +xxd MyAssembly2.dll > assembly2.hex +diff assembly1.hex assembly2.hex + +# Windows - using fc with /b flag for binary comparison +fc /b MyAssembly1.dll MyAssembly2.dll + +# Or using Format-Hex in PowerShell +Format-Hex MyAssembly1.dll | Out-File assembly1.hex +Format-Hex MyAssembly2.dll | Out-File assembly2.hex +Compare-Object (Get-Content assembly1.hex) (Get-Content assembly2.hex) +``` + +A hex diff shows the exact bytes that differ, which can help identify non-deterministic data like timestamps, GUIDs, or other embedded values. + +### Common Causes of Non-Determinism + +When debugging, look for these common issues: +- **Missing `/deterministic` flag**: Ensure `/deterministic` is enabled +- **Absolute paths**: Use `/pathmap` to normalize file paths +- **Timestamps**: Check if PDBs or other files embed build times +- **Different compiler versions**: Verify same compiler version is used +- **Different reference assembly versions**: Check MVIDs (Module Version IDs) in `.key` files +- **Environment variables**: Variables like `%LIBPATH%` can affect output +- **Source file encoding**: Ensure consistent encoding (UTF-8 with BOM recommended) +- **Generator/analyzer differences**: Verify same versions are loaded diff --git a/docs/contributing/Compiler Test Plan.md b/docs/contributing/Compiler Test Plan.md index f058d85dbcfd..2a403c2b3f41 100644 --- a/docs/contributing/Compiler Test Plan.md +++ b/docs/contributing/Compiler Test Plan.md @@ -11,6 +11,23 @@ This document provides guidance for thinking about language interactions and tes - BCL (including mono) and other customer impact - Determinism - Loading from metadata (source vs. loaded from metadata) +- VB/F# interop +- C++/CLI interop (particularly for metadata format changes, e.g. DIMs, static abstracts in interfaces, or generic attributes) +- Performance and stress testing +- Can build VS +- Check that `Obsolete` is honored for members used in binding/lowering +- LangVersion +- IL verification (file issue on `runtime` repo as needed and track [here](https://github.com/dotnet/roslyn/issues/22872)) +- Does the feature use cryptographic hashes in any way? (examples: metadata names of file-local types, extension types, assembly strong naming, PDB document table, etc.) + - Consider using non-cryptographic hash such as `XxHash128` instead. + - If you must use a cryptographic hash in the feature implementation, then use `SourceHashAlgorithms.Default`, and not any specific hash. + - A cryptographic hash must never be included in a public API name. Taking a change to the default crypto algorithm would then change public API surface, which would be enormously breaking. + - **DO NOT** allow using the value of a crypto hash in a field, method or type name + - **DO** allow using the value of a crypto hash in attribute or field values + - Any time the compiler reads in metadata containing crypto hashes, even if it's an attribute value, ensure the crypto hash algorithm name is included in the metadata (e.g. prefixing it to the hash value), so that it can be changed over time and the compiler can continue to read both metadata using both the old and new algorithms. + +## Public APIs + - Public compiler APIs (including semantic model and other APIs listed below): - GetDeclaredSymbol - GetEnclosingSymbol @@ -32,21 +49,10 @@ This document provides guidance for thinking about language interactions and tes - GetOperation (`IOperation`) - GetCFG (`ControlFlowGraph`), including a scenario with some nested conditional - DocumentationCommentId APIs -- VB/F# interop -- C++/CLI interop (particularly for metadata format changes, e.g. DIMs, static abstracts in interfaces, or generic attributes) -- Performance and stress testing -- Can build VS -- Check that `Obsolete` is honored for members used in binding/lowering -- LangVersion -- IL verification (file issue on `runtime` repo as needed and track [here](https://github.com/dotnet/roslyn/issues/22872)) - -- Does the feature use cryptographic hashes in any way? (examples: metadata names of file-local types, extension types, assembly strong naming, PDB document table, etc.) - - Consider using non-cryptographic hash such as `XxHash128` instead. - - If you must use a cryptographic hash in the feature implementation, then use `SourceHashAlgorithms.Default`, and not any specific hash. - - A cryptographic hash must never be included in a public API name. Taking a change to the default crypto algorithm would then change public API surface, which would be enormously breaking. - - **DO NOT** allow using the value of a crypto hash in a field, method or type name - - **DO** allow using the value of a crypto hash in attribute or field values - - Any time the compiler reads in metadata containing crypto hashes, even if it's an attribute value, ensure the crypto hash algorithm name is included in the metadata (e.g. prefixing it to the hash value), so that it can be changed over time and the compiler can continue to read both metadata using both the old and new algorithms. +- All newly added APIs are experimental + - Tracking issue for marking APIs as non-experimental + - APIs are marked with `[Experimental(RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = "link to tracking issue")]` + - APIs have gone through API review ## Type and members @@ -64,6 +70,7 @@ See also [types](#types) and [members](#members) lists below. - constructors (static, instance, primary) - destructors - fields (required and not) +- fixed-sized buffers - properties (including get/set/init accessors, required and not) - events (including add/remove accessors) - Parameter modifiers: ref, out, in, ref readonly, params (for array, for non-array) diff --git a/docs/contributing/Localization In Compiler Tests.md b/docs/contributing/Localization In Compiler Tests.md index a9b22b0925f5..9e6381753863 100644 --- a/docs/contributing/Localization In Compiler Tests.md +++ b/docs/contributing/Localization In Compiler Tests.md @@ -3,25 +3,25 @@ Localization in Compiler Tests The compiler tests are structured such that they can be run on a machine using any locale. This both serves as a tool to ensure the compiler does not have any localization errors but also to -ensure that developers from any locale can contribute to the project. The ability to build and +ensure that developers from any locale can contribute to the project. The ability to build and execute tests should not be limited to English speaking developers. -Our infrastructure runs the compiler test suite on `es-ES` machines for both .NET Core and .NET -Framework. This helps us ensure that compiler tests can be authored and executed on non-English +Our infrastructure runs the compiler test suite on `es-ES` machines for both .NET Core and .NET +Framework. This helps us ensure that compiler tests can be authored and executed on non-English machines without issues. ## How it works -To ensure that our tests run on any locale there is a general approach that is followed in the vast -majority of our tests. +To ensure that our tests run on any locale there is a general approach that is followed in the vast +majority of our tests. 1. All expected baselines are expressed in invariant culture. That means that say `decimal` values are expressed using `.` separators not `,`. 2. All generated baselines, usually via `Console.Writeline` inside `CompileAndVerify`, must be generated such that they produce output in invariant culture. -3. Diagnostic tests use the `VerifyDiagnostics` helpers using `en-US` values for the messages. The implementation of `VerifyDiagnostics` will request the diagnostic message in the `en-US` culture. +3. Diagnostic tests use the `VerifyDiagnostics` helpers using `en-US` values for the messages. The implementation of `VerifyDiagnostics` will request the diagnostic message in the `en-US` culture. This helps ensure the compiler has no localization product issues by forcing a consistent culture no matter the current culture of the system. -The largest source of friction that developers encounter when authoring tests is passing values to -`Console.WriteLine` that have locale dependent representations. For example `double` and `decimal` -values. +The largest source of friction that developers encounter when authoring tests is passing values to +`Console.WriteLine` that have locale dependent representations. For example `double` and `decimal` +values. ```csharp decimal d = 1.2; @@ -38,21 +38,36 @@ decimal d = 1.2; Console.WriteLine(d.ToString(CultureInfo.InvariantCulture)); ``` -This will consistently print `1.2`. +This will consistently print `1.2`. ## Exceptions -There are a few cases where it is very difficult to avoid locale dependent values. These include: +There are a few cases where it is very difficult to avoid locale dependent values. These include: -1. When a diagnostic includes a message from a thrown exception. +1. When a diagnostic includes a message from a thrown exception. 2. When a diagnostic includes a message from the underlying OS. -3. When a message is generated by a tool such as `msbuild`. +3. When a message is generated by a tool such as `msbuild`. -In those cases the message is likely to be locale dependent. Tests that have these values should be +In those cases the message is likely to be locale dependent. Tests that have these values should be marked is `[ConditionalFact(typeof(IsEnglishLocal))]` so they run on `en-US` machines only. ## Infrastructure -The Spanish machines used in our infrastructure execute with `CurrentCulture` set to `es-ES` but -`CurrentUICulture` set to `en-US`. This configuration means that roughly string formatting happens -with `es-ES` but resource lookups happen with `en-US`. This setup doesn't fully test our code base +### Windows +The Spanish Windows machines used in our infrastructure execute with `CurrentCulture` set to `es-ES` but +`CurrentUICulture` set to `en-US`. This configuration means that roughly string formatting happens +with `es-ES` but resource lookups happen with `en-US`. This setup doesn't fully test our code base so our test infrastructure will force the `CurrentUICulture` to be `CurrentCulture` when the two differ. + +This is done in the `TestBase` constructor, which all compiler test classes inherit from. When `CurrentUICulture` does not equal `CurrentCulture`, the constructor saves the original `CurrentUICulture` +and sets `CurrentUICulture` to `CurrentCulture`. The original value is restored in `Dispose` so the +change is scoped to the lifetime of each test instance. This ensures that on a Windows machine configured +with `CurrentCulture = es-ES` and `CurrentUICulture = en-US`, resource string lookups also happen in +`es-ES`, fully exercising the localization paths. + +### Linux +The Spanish Linux machines used in our infrastructure run on Ubuntu with the `LC_ALL` environment +variable set to `es_ES.UTF-8`. On .NET running on Linux, `LC_ALL` determines both `CurrentCulture` +and `CurrentUICulture`. This means that unlike the Windows configuration, both string formatting and +resource lookups will use `es-ES`. Because both culture values are already +the same, the test infrastructure does not need to force `CurrentUICulture` to `CurrentCulture` on +these machines. \ No newline at end of file diff --git a/docs/contributing/README.md b/docs/contributing/README.md index 409f48cc9d81..a870818fb9d6 100644 --- a/docs/contributing/README.md +++ b/docs/contributing/README.md @@ -15,6 +15,7 @@ Guides for contributing to this project - [Documentation for IDE CodeStyle analyzers](Documentation%20for%20IDE%20CodeStyle%20analyzers.md) - [IDE Design Process](ide_design_process.md) - [Powershell Guidelines](Powershell%20Guidelines.md) +- [Codespaces](codespaces.md) - [Testing for Interactive readiness](Testing%20for%20Interactive%20readiness.md) - [Compiler Test Plan](Compiler%20Test%20Plan.md) -- [IDE Test Plan](IDE%20Test%20Plan.md) \ No newline at end of file +- [IDE Test Plan](IDE%20Test%20Plan.md) diff --git a/docs/contributing/codespaces.md b/docs/contributing/codespaces.md new file mode 100644 index 000000000000..5b45a253d782 --- /dev/null +++ b/docs/contributing/codespaces.md @@ -0,0 +1,41 @@ +# Codespaces + +This repository provides a GitHub Codespaces configuration for full-repository Roslyn work. + +## Available configuration + +| Configuration | Default solution | Intended usage | Minimum machine | +| --- | --- | --- | --- | +| `Roslyn (.NET 10)` | `Roslyn.slnx` | Full-repository work | 8 cores, 16 GB RAM, 32 GB storage | + +The configuration uses `.devcontainer/Dockerfile`, which starts from the .NET 10 SDK image and then installs the exact SDK pinned by `global.json`. + +## Lifecycle choices + +The repo uses `.devcontainer/restore-workspace.sh ` in `updateContentCommand`. + +That placement is intentional: + +- `updateContentCommand` runs during Codespaces prebuild creation. +- `postCreateCommand` does not run during prebuild creation. +- Keeping restore in `updateContentCommand` allows GitHub Codespaces to cache the expensive first-run restore step. + +The configuration also installs `gh` and `pwsh` through dev container features so the container matches common Roslyn command-line workflows more closely. + +## Recommended repository-level GitHub settings + +These settings live in the repository's **Settings > Codespaces** page rather than in the repo itself. + +1. Enable a prebuild for the default branch using `.devcontainer/devcontainer.json`. +2. Start with the **On configuration change** trigger to keep storage and Actions usage under control. +3. Keep retained prebuild versions at `2` unless you have a clear need for a longer rollback window. +4. Limit prebuild regions to the regions your team actually uses. + +## Validation checklist + +After changing Codespaces settings or the devcontainer configuration: + +1. Create a fresh codespace from `.devcontainer/devcontainer.json`. +2. Confirm the expected default solution loads in VS Code. +3. Confirm `gh --version` and `pwsh --version` work in the terminal. +4. Confirm the restore step completes successfully for the selected solution. diff --git a/docs/contributing/target-framework-strategy.md b/docs/contributing/target-framework-strategy.md index 7f198539338b..e4e1a7cca069 100644 --- a/docs/contributing/target-framework-strategy.md +++ b/docs/contributing/target-framework-strategy.md @@ -26,8 +26,9 @@ Projects in our repository should include the following values in `` setting. Instead our repo uses the above values and when inside source build or VMR our properties are initialized with arcade properties. diff --git a/docs/features/file-based-programs-vscode.md b/docs/features/file-based-programs-vscode.md index e18e0600f582..4769c34b0c1d 100644 --- a/docs/features/file-based-programs-vscode.md +++ b/docs/features/file-based-programs-vscode.md @@ -42,17 +42,72 @@ record Data(string field1, int field2); This basically works by having the `dotnet` command line interpret the `#:` directives in source files, produce a C# project XML document in memory, and pass it off to MSBuild. The in-memory project is sometimes called a "virtual project". -## Miscellaneous files changes +## Rich miscellaneous files There is a long-standing backlog item to enhance the experience of working with miscellaneous files ("loose files" not associated with any project). We think that as part of the "file-based program" work, we can enable the following in such files without substantial issues: - Syntax diagnostics. - Intellisense for the "default" set of references. e.g. those references which are included in the project created by `dotnet new console` with the current SDK. +- In certain cases, we can even enable semantic diagnostics, with reasonable confidence that the resulting errors are useful to the user. -### Heuristic -The IDE considers a file to be a file-based program, if: -- It has any `#:` directives which configure the file-based program project, or, -- It has any top-level statements. -Any of the above is met, and, the file is not included in an ordinary `.csproj` project (i.e. it is not part of any ordinary project's list of `Compile` items). +These changes to misc files behavior are called "rich miscellaneous files". + +The implementation strategy is: the editor creates a "canonical misc files project" under the temp directory, and uses the resulting project info as a "base project" for loose files that are opened in the IDE. + +## File-based app detection + +A C# file has multiple possible *classifications* in the editor: +- **Project-Based App**. The file is part of an ordinary `.csproj` project. +- **File-Based App**. The file is part of a "file-based app" project, i.e. it is either the entry point of a file-based app or it is `#:include`d by the entry point of the same. +- **Miscellaneous File With Standard References and Semantic Errors**. The file is a valid entry point for either a file-based app, but lacks the `#:`/`#!` directives which give us high certainty that this is the user's intent. + - Tooling will light up accordingly, showing syntax errors, semantic errors, semantic info for the core library, etc. See *Rich miscellaneous files* section above. + - These files will not be restored. +- **Miscellaneous File With Standard References**. The file isn't part of any project, and heuristics indicate it's not intended to be a file-based app. The file uses the regular C# language (not the `.csx` scripting dialect). + - Syntax errors and semantic info for the core library will appear in these files. + - Semantic errors will not appear in these files. +- **Miscellaneous File With No References**. The file isn't part of any project. It may even be a `.csx`, `.razor`, or other non-.cs type. + - These files do not have any references to the core library, and do not show semantic errors. + - Syntax errors, go-to-def on declarations in the same file, etc., may work. + - When `enableFileBasedPrograms` is disabled, this classification is generally used instead of one of the *miscellaneous file with standard references* or *file-based app* classifications above. + +**NOTE:** This is intended to be a living document, and for the set of checks and classifications to possibly change over time depending on our needs. + +This is the decision tree for determining how to classify a C# file: + +1. **Is the file in a currently loaded project?** + - **Yes** → Classify as **Project-Based App** + - **No** → Continue to next check + +2. **Is `enableFileBasedPrograms` enabled?** (default: `true` in release) + - **No** → Classify as **Miscellaneous File With No References** + - **Yes** → Continue to next check + +3. **Is the file a regular C# file? (i.e. not a `.csx` script, and not a file using a language besides C#)** + - **No** → Classify as **Miscellaneous File With No References** + - **Yes** → Continue to next check + +4. **Does the file have an absolute path and exist on disk?** (i.e. it is not a "virtual document" created for a new, not-yet-saved file, or similar.) + - **No** → Classify as **Classify as Miscellaneous File With Standard References** + - **Yes** → Continue to next check + +5. **Does the file have `#:` or `#!` directives?** + - **Yes** → Classify as **File-Based App**. Restore if needed and show semantic errors. + - **No** → Continue to next check + +6. **Is `enableFileBasedProgramsWhenAmbiguous` enabled?** (default: `false` in release, `true` in prerelease) + - **No** → Classify as **Miscellaneous File With Standard References** + - **Yes** → Continue to heuristic detection + +**Heuristic Detection (when `enableFileBasedProgramsWhenAmbiguous: true`):** + +7. **Are top-level statements present?** + - **No** → Classify as **Miscellaneous File With Standard References** + - **Yes** → Continue to next check + +8. **Is the file included in a `.csproj` cone?** + - "Cone" means that a containing directory, at some level of nesting, has a `.csproj` file in it. + - Note that this specific check is only performed at the time the file is opened. We think that the typical case is that the user will load a new project they are creating. Loading the project will cause the file to start being treated as project-based app per (1). If the user does not load the new project, then stale diagnostics may remain present until the file is closed and re-opened. + - **Yes** → Classify as **Miscellaneous File With Standard References** (wait for project to load) + - **No** → Classify as **Miscellaneous File With Standard References and Semantic Errors** ### Opt-out @@ -62,3 +117,93 @@ We also have a second, finer-grained opt-out flag `dotnet.projects.enableFileBas > [!NOTE] > The second flag is being used on a short-term basis while we work out the set of heuristics and cross-component APIs needed to efficiently and satisfactorily resolve whether a file with top-level statements but no directives is a file-based program in the context of a complex workspace. + +## LSP handling of file-based apps + +When a C# file adds `#:` or `#!` directives, it becomes a file-based app. +Conceptually, what happens is: the file becomes both a C# source file, and a project file, in one. + +Conversely, when all `#:`/`#!` directives are removed, it stops being a project file, and goes back to being a C# source file only. In this scheme, we think of a file which contains `#:` as being the "entry point file" of the file-based app. + +We are adding support for an `#:include` directive to file-based apps, which lets users point at single files or `*` globs of C# source files, or other additional files (content, resources, etc.), which should be included in the file-based app project. This makes file-based app projects behave much more like ordinary projects in the workspace. In particular, we can have situations like the following: +- `Util.cs` (an ordinary source file) +- `App1.cs`, a file-based app entry point containing `#:include Util.cs` +- `App2.cs`, a file-based app entry point also containing `#:include Util.cs` +- `MyProject.csproj`, also containing `` + +Because all these projects are simply added as projects to the host workspace, it's expected that features like "active project context" and multi-targeting-aware Quick Info "just work" with all of them. + +One key assumption we are making is: it is not valid for a file-based app *entry point* to be a member of an ordinary project. e.g. you cannot have the following: +- `Util.cs` (an ordinary source file) +- `App1.cs`, a file-based app entry point containing `#:include Util.cs` +- `MyProject.csproj`, containing `` **<-- This part is considered malformed** + +An error is reported generally for presence of `#:`/`#!` directives in ordinary projects. Depending on the order that things load, such files may or may not also be detected as file-based app entry points. + +In this case we want the user to do one of 2 things to resolve the issue: +1. Delete the `#:`/`#!` directives. We will unload the file as file-based app in this case. +2. Remove the file-based app entry point as a member of any ordinary project(s). + +We expect the appropriate project system(s) to be able to observe either of the above changes and move the workspace into a "healthy" state once the user has corrected the error. + +### `FileBasedProgramsProjectSystem` + +Manages projects for file-based programs and miscellaneous files. + +This project system effectively performs the classification process described in [File-based app detection](#file-based-app-detection) when a design-time build is performed for the project, and transitions the state of the project to match the latest classification. + +This uses the file-based program entry point file, translates it to a virtual msbuild project, then runs a design-time build on that project. If it detects missing assets, it may also restore the virtual project. + +It uses file watchers to watch the project globs and redo the design time build on relevant changes, such as changes to `#:` directives. + +## Automatic discovery + +The Roslyn LSP will automatically discover and load file-based apps in the opened workspace folders. The user can opt out of this discovery process by setting `"dotnet.fileBasedApps.enableAutomaticDiscovery": false`. +For the first release of the feature in the VS Code C# extension, the setting will be disabled-by-default in the stable release channel and enabled-by-default in the prerelease channel. + +Certain subfolders in a workspace are excluded from this discovery process: +- Any folders which contain a `.csproj` file. +- Any folders with names conventionally reserved for build artifacts, such as `artifacts`, `bin`, and `obj`. +- Any folders marked "hidden" in the file system. `.git` and `.vs` typically fall into this. + +The first time discovery is performed in a workspace, the LSP will read all `.cs` files in the opened workspace folders which are not excluded by the above conditions. If the file content starts with `#!`, it is marked as a file-based app and loaded. + +A cache file is created after each discovery pass and stored in the user temp directory. This file holds: +- The time that the previous discovery pass started. +- Paths of file-based apps found during the last discovery pass. +- Paths of folders that were found to contain `.csproj` files during the last discovery pass. + +The cache data allows the following optimizations in subsequent discovery passes: +- Allows not reading any C# files whose last write time is older than the cached time. +- Allows reducing the number of times we list files in directories whose last write time is older than the cached time. + +### `#!` requirement + +This design requires files to start with `#!` in order to participate in discovery. +Specifically, a discoverable file must start with either the byte sequence `0x23, 0x21` (ASCII/UTF-8 `#!`), or the byte sequence `0xEF, 0xBB, 0xBF, 0x23, 0x21` (UTF-8 BOM followed by `#!`). + +The reason for this is: we anticipate adding support for `#:` to non-entry-point files. This means that having `#:` is not going to be enough to identify a file as definitely the entry point. + +Instead, it will be necessary to search for both `#:` and top-level statements at a minimum. This cost is acceptable for files that were explicitly opened in the editor, but is a bit steep for a broad discovery pass. + +For this reason, we intend to put `#!`-at-start as a standard for entry points of file-based apps. We plan on shipping an analyzer which reports a warning in files which contain both `#:include` and top-level statements, but do not have `#!` at the top. + +## Future considerations + +This section is not intended to serve as permanent documentation but as more of a roadmap for a series of changes we may make in this area in the near future. It should not be necessary to read/understand this in order to evaluate a PR currently under review. i.e. anything that the current PR is actually implementing is covered in previous sections. + +### Treating files with no directives as file-based app entry points + +**Miscellaneous File With Standard References and Semantic Errors**, is a designation we essentially have in order to avoid restoring things we aren't 100% sure are file-based apps. This particularly includes files which have top-level statements, but no `#:`/`#!` directives. + +We may want to make a change in the future, to stop using this designation for files which exist on disk, and instead classify files not part of an ordinary project, containing top-level statements, and with no csproj-in-cone, as being file-based apps. This would improve accuracy in the editor in certain cases, and make it easier to do things like avoid showing the *This is a miscellanous file, things may be broken* popup. + +### Allowing non-entry-point files to contain `#:` directives + +We are considering adding support for non-entry-point files to contain `#:` in the future. In this case, we would need an additional bit of information to distinguish entry points from non-entry-points. +For *multi-file file-based apps*, users should use a `#!` at the top of the entry point file to make it easy to identify. +For *single-file file-based apps*, we think that just using `#:` and top-level statements together should be enough, to identify a file that was explicitly opened in the editor as an entry point. + +### Checking top-level statements presence without doing a full parse + +Currently there are cases where we may end up needing to do an additional parse of a file just to check if it contains top-level statements. This is generally a situation we'd like to avoid, and, would prefer to either use a pattern where the file already exists in some project and has a syntax tree we can check incrementally, or, that we devise some other solution for performing our heuristics which doesn't require a full parse. diff --git a/docs/features/incremental-generators.cookbook.md b/docs/features/incremental-generators.cookbook.md index c6ed1090f73c..1057e8a56ec3 100644 --- a/docs/features/incremental-generators.cookbook.md +++ b/docs/features/incremental-generators.cookbook.md @@ -609,6 +609,21 @@ In the users project file, the user can now annotate the individual additional f ``` +Note that MSBuild properties passed to source generators via `CompilerVisibleProperty` are written into and read from an editorconfig file, [resulting in data loss for non-trivial property values](https://github.com/dotnet/roslyn/issues/51692). One possible workaround is to use a build task to apply a transport encoding preventing the data loss; as an example a semicolon separated list can be converted to a space separated list (`;` is the editorconfig comment character): + +```xml + + + + + + + <_MyInterpolatorsNamespaces>$([System.String]::Copy('$(InterpolatorsNamespaces)').Replace(';', ' ')) + + + +``` + **Full Example:** MyGenerator.props: diff --git a/docs/features/source-generators.cookbook.md b/docs/features/source-generators.cookbook.md index 970dd6792c76..dee32fb7ac8d 100644 --- a/docs/features/source-generators.cookbook.md +++ b/docs/features/source-generators.cookbook.md @@ -583,6 +583,21 @@ In the users project file, the user can now annotate the individual additional f ``` +Note that MSBuild properties passed to source generators via `CompilerVisibleProperty` are written into and read from an editorconfig file, [resulting in data loss for non-trivial property values](https://github.com/dotnet/roslyn/issues/51692). One possible workaround is to use a build task to apply a transport encoding preventing the data loss; as an example a semicolon separated list can be converted to a space separated list (`;` is the editorconfig comment character): + +```xml + + + + + + + <_MyInterpolatorsNamespaces>$([System.String]::Copy('$(InterpolatorsNamespaces)').Replace(';', ' ')) + + + +``` + **Full Example:** MyGenerator.props: diff --git a/docs/roslyn-analyzers/acquisition-strategy.md b/docs/roslyn-analyzers/acquisition-strategy.md new file mode 100644 index 000000000000..f920ec765b95 --- /dev/null +++ b/docs/roslyn-analyzers/acquisition-strategy.md @@ -0,0 +1,100 @@ +# Roslyn Analyzer acquisition strategy + +This document describes how consumers should acquire the **Microsoft.CodeAnalysis.* (Roslyn) analyzer packages** over time, and why the acquisition model changes across major versions. + +## Goals + +1. **Reliable analyzer execution**: analyzers should run on a compiler/host that they are compatible with. +2. **Lower maintenance cost**: reduce long-term burden of supporting extremely old compiler hosts. +3. **Simpler acquisition**: move from “add NuGet packages” toward “enable via MSBuild properties”, where possible. +4. **Version alignment**: analyzers should be **version-matched to the compiler** that executes them. + +--- + +## Recommended guidance + +### If you need maximum host compatibility +- Prefer **<= 3.11.0** analyzer packages built from `dotnet/roslyn-analyzer`, especially when builds must run on older compilers/hosts. + +### If you are on modern SDKs and want the newest analyzer line +- Use **5.0+** analyzer packages, ensuring your build uses the **.NET 9 SDK or higher**. + +### Plan for the SDK-based future +- Expect a transition from `PackageReference`-based acquisition to **SDK-provided analyzers** enabled by configuration properties. +- Prefer central configuration (for example, `Directory.Build.props`) to ease future migration. + +--- + +## Background and timeline + +### Analyzer packages <= 3.11.0 + +For package versions **less than or equal to 3.11.0**: + +- The analyzer packages were built from the **`dotnet/roslyn-analyzer`** repository. +- These versions were designed to support running on **very old Roslyn compilers and compiler hosts** (for example, older Visual Studio / older SDK toolsets). +- This required significant **backwards-compatibility code and testing**, increasing maintenance overhead. + +**Implication for consumers:** these packages were broadly compatible with older toolchains, making them a good choice when analyzer execution had to work across older environments. + +--- + +### Analyzer packages 5.0+ (post-merge into dotnet/roslyn) + +The **`dotnet/roslyn-analyzer`** repository was merged into **`dotnet/roslyn`**. As part of that consolidation: + +- A large amount of the special-case compatibility logic for very old hosts was **removed** to reduce maintenance burden. +- The newer packages adopt a compatibility posture aligned with modern SDK/compiler shipping. + +For the new **5.0** packages: + +- They **require the compiler from Visual Studio 17.12/.NET 9 SDK or higher**. + +**Implication for consumers:** using 5.0 analyzers assumes your build uses a sufficiently new compiler/SDK. If your build environment is older, these analyzers are not a supported choice. + +--- + +## Current acquisition model (today) + +Today, acquisition is typically done via **NuGet PackageReference**, for example: + +- Add analyzer packages to a project (`.csproj`) or centrally in `Directory.Build.props`. +- Ensure the build environment uses a compiler/SDK new enough to run that analyzer version (notably for 5.0+). + +This model works but has downsides: + +- Consumers must discover and choose package versions. +- Version skew can happen: analyzers may be newer/older than the compiler executing them. +- Upgrading analyzers can implicitly require upgrading the SDK/compiler host. + +--- + +## Future direction: ship analyzers in the .NET SDK + +The long-term goal is to ship these analyzer packages **in the .NET SDK**. Currently Roslyn ships their CodeStyle analyzers in the SDK. Consumers opt-in to using them via the `EnforceCodeStyleInBuild` property. + +### Why this is better + +When analyzers ship in the SDK: + +- The analyzers can be **version-matched to the compiler** automatically. +- The acquisition experience becomes simpler: + - instead of adding `PackageReference`, you enable analyzers by setting **MSBuild properties** in your `.csproj` or `Directory.Build.props`. +- The SDK can ensure the analyzer/compiler pairing is known-good, reducing compatibility surprises. + +### What this will look like for consumers + +Conceptually, consumers will move from: + +- “Add analyzer packages via NuGet” + +to: + +- “Enable analyzers via properties” + +configured in one of: + +- a project file (`.csproj`) +- `Directory.Build.props` (recommended for repository-wide policy) + +*(Exact property names and final UX may evolve as the SDK integration is finalized.)* diff --git a/docs/roslyn-language-server-copilot-plugin.md b/docs/roslyn-language-server-copilot-plugin.md new file mode 100644 index 000000000000..acde29824339 --- /dev/null +++ b/docs/roslyn-language-server-copilot-plugin.md @@ -0,0 +1,124 @@ +# Roslyn Language Server - Copilot Plugin + +The `roslyn-language-server` is a .NET tool that provides C# language intelligence (go-to-definition, find references, diagnostics, etc.) to AI coding agents via the [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/). It is distributed as a plugin through the [dotnet/skills](https://github.com/dotnet/skills) marketplace, enabling automatic installation and use in tools like GitHub Copilot CLI. + +## Quick Start + +### Install the plugin + +In your agent (Copilot CLI, etc.), add the marketplace and install the `dotnet` plugin: + +``` +/plugin marketplace add dotnet/skills +/plugin install dotnet@dotnet-agent-skills +``` + +Restart the agent to load the plugin. Once installed, the agent will automatically have access to C# language intelligence for `.cs` files through the LSP. + +### What it provides + +With the plugin active, the agent gains LSP-powered capabilities for C# code: + +- **Go to Definition** — navigate to where a symbol is defined +- **Find References** — find all usages of a symbol +- **Hover** — get type information and documentation +- **Diagnostics** — see compiler errors and warnings +- **Document Symbols** — list all symbols in a file +- **And more** — any LSP feature supported by the Roslyn language server + +## Prerequisites + +- **.NET 10 SDK** must be installed and available on `PATH`. Your project can still use an older, supported SDK. + +## Automatic Project Loading + +The language server automatically discovers and loads projects using the following strategy (evaluated in order): + +### 1. VS Code Settings (`dotnet.defaultSolution`) + +If a `.vscode/settings.json` file exists in the workspace folder, the server reads the `dotnet.defaultSolution` setting: + +```jsonc +// .vscode/settings.json +{ + "dotnet.defaultSolution": "src/MyApp.sln" +} +``` + +- **Relative or absolute paths** to a `.sln` or `.slnx` file are supported. +- Set to `"disable"` to prevent the server from loading any solution or projects automatically: + ```jsonc + { + "dotnet.defaultSolution": "disable" + } + ``` + +### 2. Single Solution File at the Root + +If there is exactly **one** `.sln` or `.slnx` file at the root of the workspace folder, the server will automatically load that solution. + +### 3. Individual Project Discovery + +As a fallback, the server recursively discovers all `.csproj` files within the workspace folders and loads them individually. + +## Troubleshooting + +### Verify LSP configuration is found + +- In Copilot CLI, running the `/lsp show` should show the server configured for C#: +``` + Plugin-configured servers: + • csharp: (.cs) [from dotnet] +``` + +### Viewing LSP Logs + +Copilot CLI writes LSP server logs to the `.copilot/logs/` directory in your home folder. To inspect the language server's output: + +- **macOS / Linux:** `~/.copilot/logs/` +- **Windows:** `%USERPROFILE%\.copilot\logs\` + +Look for log files related to `csharp` or `roslyn-language-server`. These contain the server's startup output, project loading progress, and any errors encountered. This is the first place to check when the language server isn't behaving as expected. + +### The tool isn't being found + +- Ensure `dotnet` is on your `PATH`. +- Check for a `nuget.config` (repo, user, or machine-level) that restricts package sources; `dnx` uses NuGet sources to resolve tools. +- Verify `nuget.org` (or an other feed that mirrors `roslyn-language-server`) is enabled, for example with `dotnet nuget list source`. +- Try installing the tool manually: `dotnet tool install -g roslyn-language-server --prerelease`. + +### Project load issues + +If the LSP logs show failures loading projects + +- Verify that a compatible .NET SDK is installed by running `dotnet --version`. +- Check that your project builds successfully with `dotnet build` before using the language server. +- For large repositories with multiple solutions, configure `dotnet.defaultSolution` in `.vscode/settings.json` to specify which solution to load. + +### Performance + +- Loading a solution with many projects may take some time. The server reports progress to the client during loading. +- For large repositories, prefer loading a specific solution (via `dotnet.defaultSolution`) rather than relying on individual project discovery, which may load test projects and other projects you don't need. + +## Related Links + +- [dotnet/skills repository](https://github.com/dotnet/skills) — Plugin marketplace for .NET agent skills + +## How It Works + +The plugin is configured in the [dotnet/skills](https://github.com/dotnet/skills) repository via [`plugins/dotnet/lsp.json`](https://github.com/dotnet/skills/blob/main/plugins/dotnet/lsp.json): + +When the agent opens a workspace containing `.cs` files, it will: + +1. **Install and run** the [`roslyn-language-server`](https://www.nuget.org/packages/roslyn-language-server) .NET tool on-the-fly using `dotnet dnx` (which downloads and caches the tool automatically; `--yes` skips confirmation and `--prerelease` allows prerelease versions). +2. **Communicate over stdio** (`--stdio`) using the Language Server Protocol. +3. **Automatically discover and load projects** (`--autoLoadProjects`) so that the agent immediately has full semantic understanding of the codebase. + +### Command-Line Options + +| Option | Description | +|--------|-------------| +| `--stdio` | Use stdio for LSP communication (required for most agent integrations) | +| `--autoLoadProjects` | Automatically discover and load projects from workspace folders | +| `--logLevel ` | Minimum log verbosity (default: `Information`) | +| `--debug` | Launch the debugger on startup | diff --git a/docs/wiki/Diagnosing-Project-System-Build-Errors.md b/docs/wiki/Diagnosing-Project-System-Build-Errors.md index 120e142e0f9e..529cdf594f7e 100644 --- a/docs/wiki/Diagnosing-Project-System-Build-Errors.md +++ b/docs/wiki/Diagnosing-Project-System-Build-Errors.md @@ -17,9 +17,20 @@ There are a few ways to tell: 3. If you're using Visual Studio 2015 Update 2 or later, look for warning IDE0006 in the error list: ![IDE0006 error example](images/design-time-build-errors/ide0006.png) -## How do I get log files to diagnose what is happening in Visual Studio 2022? +## How do I get log files to diagnose what is happening in Visual Studio 2026? -1. Install the [Project System Tools 2022 Extension from the Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=VisualStudioProductTeam.ProjectSystemTools2022) +1. Go under the View menu, choose Other Windows, and then Command Window. +2. The Command Window opens up. In it, type `Project.LogRoslynWorkspaceStructure`: + + ![Example screenshot of the Command Window](images/design-time-build-errors//typing-in-command-window.png) + +3. Press enter. A prompt will appear asking you to save an XML file. This will prompt to save an XML file, and the process may take some time. Attach this item privately to your problem report if you have one. + +## How do I get log files to diagnose what is happening in Visual Studio 2017, 2019, or 2022? + +1. Install the Project System Tools Extension from the Visual Studio Marketplace: + - [Version for Visual Studio 2022](https://marketplace.visualstudio.com/items?itemName=VisualStudioProductTeam.ProjectSystemTools2022) + - [Version for Visual Studio 2017 and 2019](https://marketplace.visualstudio.com/items?itemName=VisualStudioProductTeam.ProjectSystemTools) 2. Restart Visual Studio as a part of installing the extension. 3. Close Visual Studio again, find your solution file on disk, and delete the .vs hidden folder that is alongside your solution. You'll have to show hidden files if you don't see it. 4. Open Visual Studio. Don't open your Solution yet. diff --git a/docs/wiki/Troubleshooting-tips.md b/docs/wiki/Troubleshooting-tips.md index e7b9318bf60e..bb267d2fb49f 100644 --- a/docs/wiki/Troubleshooting-tips.md +++ b/docs/wiki/Troubleshooting-tips.md @@ -84,3 +84,4 @@ There are three significant candidates to investigate: Use `/p:Features=debug-determinism` to create an additional output file that documents all the inputs to a particular compilation. The file is written next to the compilation output and has a `.key` suffix. Comparing those files between slow and fast runs helps detect pertinent changes (new inputs, new references, etc). + See [Generate a Deterministic Key File](../compilers/Deterministic%20Inputs.md#1-generate-a-deterministic-key-file) for more details. diff --git a/docs/wiki/images/design-time-build-errors/typing-in-command-window.png b/docs/wiki/images/design-time-build-errors/typing-in-command-window.png new file mode 100644 index 000000000000..f8461fc00204 Binary files /dev/null and b/docs/wiki/images/design-time-build-errors/typing-in-command-window.png differ diff --git a/eng/Packages.props b/eng/Packages.props index 467cb4c8f6e9..c1979f78e55b 100644 --- a/eng/Packages.props +++ b/eng/Packages.props @@ -14,17 +14,17 @@ We should try to keep this version in sync with the version of app-local runtime in VS. --> - 8.0.10 - 8.0.10 + 10.0.1 + 10.0.1 <_xunitVersion>2.9.2 - 2.1.0 + 2.1.6 2.14.1 @@ -252,8 +252,8 @@ - - + + @@ -288,6 +288,8 @@ + + @@ -314,7 +316,7 @@ Infra --> - + diff --git a/eng/Publishing.props b/eng/Publishing.props index a0e65bf33255..32ca1f3c52b7 100644 --- a/eng/Publishing.props +++ b/eng/Publishing.props @@ -14,6 +14,12 @@ + + + + + + - - 5.3.0-2.25625.1 - 5.3.0-2.25625.1 - 5.3.0-2.25625.1 - 5.3.0-2.25625.1 - - 11.0.100-alpha.1.26060.102 - 5.4.0-2.26060.102 - 3.0.0-alpha.1.26060.102 - + + 10.0.0-beta.26201.4 + 10.0.0-beta.26201.4 + 10.0.0-beta.26201.4 + + 1.1.0-beta.25161.2 + 10.0.1 10.0.1 10.0.1 @@ -25,7 +22,9 @@ This file should be imported by eng/Versions.props 10.0.1 10.0.1 10.0.1 + 5.7.0-1.26202.104 10.0.1 + 3.0.0-preview.4.26202.104 10.0.1 10.0.1 10.0.1 @@ -41,27 +40,23 @@ This file should be imported by eng/Versions.props 10.0.1 10.0.1 10.0.1 - - 11.0.0-beta.26055.1 - 11.0.0-beta.26055.1 - 11.0.0-beta.26055.1 - + + 5.6.0-2.26172.2 + 5.6.0-2.26172.2 + 5.6.0-2.26172.2 + 5.6.0-2.26172.2 + 2.0.0 - - 1.1.0-beta.25161.2 - - $(MicrosoftCodeAnalysisPackageVersion) - $(MicrosoftCodeAnalysisAnalyzersPackageVersion) - $(MicrosoftCodeAnalysisAnalyzerUtilitiesPackageVersion) - $(MicrosoftNetCompilersToolsetPackageVersion) - - $(MicrosoftDotNetFileBasedProgramsPackageVersion) - $(RoslynDiagnosticsAnalyzersPackageVersion) - $(SystemCommandLinePackageVersion) - + + $(MicrosoftDotNetArcadeSdkPackageVersion) + $(MicrosoftDotNetHelixSdkPackageVersion) + $(MicrosoftDotNetXliffTasksPackageVersion) + + $(MicrosoftDotNetDarcLibPackageVersion) + $(MicrosoftBclAsyncInterfacesPackageVersion) $(MicrosoftExtensionsConfigurationPackageVersion) $(MicrosoftExtensionsDependencyInjectionPackageVersion) @@ -72,7 +67,9 @@ This file should be imported by eng/Versions.props $(MicrosoftExtensionsOptionsPackageVersion) $(MicrosoftExtensionsOptionsConfigurationExtensionPackageVersion) $(MicrosoftExtensionsPrimitivesPackageVersion) + $(RoslynDiagnosticsAnalyzersPackageVersion) $(SystemCollectionsImmutablePackageVersion) + $(SystemCommandLinePackageVersion) $(SystemComponentModelCompositionPackageVersion) $(SystemCompositionPackageVersion) $(SystemConfigurationConfigurationManagerPackageVersion) @@ -88,13 +85,12 @@ This file should be imported by eng/Versions.props $(SystemTextJsonPackageVersion) $(SystemThreadingTasksDataflowPackageVersion) $(SystemWindowsExtensionsPackageVersion) - - $(MicrosoftDotNetArcadeSdkPackageVersion) - $(MicrosoftDotNetHelixSdkPackageVersion) - $(MicrosoftDotNetXliffTasksPackageVersion) - + + $(MicrosoftCodeAnalysisPackageVersion) + $(MicrosoftCodeAnalysisAnalyzersPackageVersion) + $(MicrosoftCodeAnalysisAnalyzerUtilitiesPackageVersion) + $(MicrosoftNetCompilersToolsetPackageVersion) + $(MicrosoftDiaSymReaderPackageVersion) - - $(MicrosoftDotNetDarcLibPackageVersion) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 161d9f75da08..63bc92a1e40c 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,15 +1,15 @@ - + - + https://github.com/dotnet/roslyn - 5dd606bb21208dfc6fd3d9b07ae963a93248483b + 6e85e46dc82d3eefb46eca37e213c5d32093a8ff - + https://github.com/dotnet/dotnet - db3b7424251b47d9474dbbe3b4fa7a81fe2915b7 + 0d6cc667e66d7bec23439335118137f78c70306c @@ -115,51 +115,47 @@ - + https://github.com/dotnet/arcade - a4691bde931c20dfe44f556357cb450c83df360c + 2ad6e0a00c692279222642dbcd6e72eb21572d93 - + https://github.com/dotnet/arcade - a4691bde931c20dfe44f556357cb450c83df360c + 2ad6e0a00c692279222642dbcd6e72eb21572d93 https://github.com/dotnet/symreader 27e584661980ee6d82c419a2a471ae505b7d122e - + https://github.com/dotnet/roslyn - 5dd606bb21208dfc6fd3d9b07ae963a93248483b + 6e85e46dc82d3eefb46eca37e213c5d32093a8ff - + https://github.com/dotnet/arcade - a4691bde931c20dfe44f556357cb450c83df360c + 2ad6e0a00c692279222642dbcd6e72eb21572d93 https://github.com/dotnet/arcade-services 8b5a2ffee4f4097893b7fc670f7d86d84c8c841f - - https://github.com/dotnet/dotnet - db3b7424251b47d9474dbbe3b4fa7a81fe2915b7 - https://github.com/dotnet/dotnet fad253f51b461736dfd3cd9c15977bb7493becef - + https://github.com/dotnet/roslyn - 5dd606bb21208dfc6fd3d9b07ae963a93248483b + 6e85e46dc82d3eefb46eca37e213c5d32093a8ff - + https://github.com/dotnet/roslyn - 5dd606bb21208dfc6fd3d9b07ae963a93248483b + 6e85e46dc82d3eefb46eca37e213c5d32093a8ff - + https://github.com/dotnet/dotnet - db3b7424251b47d9474dbbe3b4fa7a81fe2915b7 + 0d6cc667e66d7bec23439335118137f78c70306c diff --git a/eng/Versions.props b/eng/Versions.props index de8e1e925813..531fbf539546 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -5,9 +5,9 @@ 5 - 4 + 7 0 - 2 + 1 $(PreReleaseVersionLabel)-test $(MajorVersion).$(MinorVersion).$(PatchVersion) - 17.13.0 + 17.14.1 diff --git a/eng/build.ps1 b/eng/build.ps1 index abf34996d85c..e60ef34be722 100644 --- a/eng/build.ps1 +++ b/eng/build.ps1 @@ -167,9 +167,11 @@ function Process-Arguments() { $script:useGlobalNuGetCache = $false $script:collectDumps = $true $script:testDesktop = ![System.Boolean]::Parse($officialSkipTests) + $script:buildTests = !([System.Boolean]::Parse($officialSkipTests)) $script:applyOptimizationData = ![System.Boolean]::Parse($officialSkipApplyOptimizationData) } else { $script:applyOptimizationData = $false + $script:buildTests = $null } if ($binaryLogName -ne "") { @@ -266,6 +268,7 @@ function BuildSolution() { $generateDocumentationFile = if ($skipDocumentation) { "/p:GenerateDocumentationFile=false" } else { "" } $roslynUseHardLinks = if ($ci) { "/p:ROSLYNUSEHARDLINKS=true" } else { "" } + $dotnetBuildTests = if ($buildTests -ne $null -and !$buildTests) { "/p:DotNetBuildTests=false" } else { "" } try { MSBuild $toolsetBuildProj ` @@ -294,6 +297,7 @@ function BuildSolution() { $msbuildWarnAsError ` $generateDocumentationFile ` $roslynUseHardLinks ` + $dotnetBuildTests ` @properties } finally { diff --git a/eng/build.sh b/eng/build.sh index 5537501d6a40..0ce94949b1e7 100755 --- a/eng/build.sh +++ b/eng/build.sh @@ -41,7 +41,7 @@ usage() echo " --sourceBuild Build the repository in source-only mode" echo " --productBuild Build the repository in product-build mode." echo " --fromVMR Build the repository in product-build mode." - echo " --solution Solution to build (default is Compilers.slnf)" + echo " --solution Solution to build (default is Roslyn.slnx)" echo "" echo "Command line arguments starting with '/p:' are passed through to MSBuild." } @@ -86,7 +86,7 @@ properties=() source_build=false product_build=false from_vmr=false -solution_to_build="Compilers.slnf" +solution_to_build="Roslyn.slnx" args="" @@ -383,7 +383,7 @@ fi if [[ "$test_core_clr" == true ]]; then runtests_args="" - if [[ -n "$test_compiler_only" ]]; then + if [[ "$test_compiler_only" == true ]]; then runtests_args="$runtests_args $(GetCompilerTestAssembliesIncludePaths)" fi diff --git a/eng/common/SetupNugetSources.ps1 b/eng/common/SetupNugetSources.ps1 index fc8d618014e0..65ed3a8adef0 100644 --- a/eng/common/SetupNugetSources.ps1 +++ b/eng/common/SetupNugetSources.ps1 @@ -1,6 +1,7 @@ # This script adds internal feeds required to build commits that depend on internal package sources. For instance, -# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables -# disabled internal Maestro (darc-int*) feeds. +# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. Similarly, +# dotnet-eng-internal and dotnet-tools-internal are added if dotnet-eng and dotnet-tools are present. +# In addition, this script also enables disabled internal Maestro (darc-int*) feeds. # # Optionally, this script also adds a credential entry for each of the internal feeds if supplied. # @@ -173,4 +174,16 @@ foreach ($dotnetVersion in $dotnetVersions) { } } +# Check for dotnet-eng and add dotnet-eng-internal if present +$dotnetEngSource = $sources.SelectSingleNode("add[@key='dotnet-eng']") +if ($dotnetEngSource -ne $null) { + AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName "dotnet-eng-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-eng-internal/nuget/$feedSuffix" -Creds $creds -Username $userName -pwd $Password +} + +# Check for dotnet-tools and add dotnet-tools-internal if present +$dotnetToolsSource = $sources.SelectSingleNode("add[@key='dotnet-tools']") +if ($dotnetToolsSource -ne $null) { + AddOrEnablePackageSource -Sources $sources -DisabledPackageSources $disabledSources -SourceName "dotnet-tools-internal" -SourceEndPoint "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-tools-internal/nuget/$feedSuffix" -Creds $creds -Username $userName -pwd $Password +} + $doc.Save($filename) diff --git a/eng/common/SetupNugetSources.sh b/eng/common/SetupNugetSources.sh index b97cc536379d..b2163abbe71b 100644 --- a/eng/common/SetupNugetSources.sh +++ b/eng/common/SetupNugetSources.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # This script adds internal feeds required to build commits that depend on internal package sources. For instance, -# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. In addition also enables -# disabled internal Maestro (darc-int*) feeds. +# dotnet6-internal would be added automatically if dotnet6 was found in the nuget.config file. Similarly, +# dotnet-eng-internal and dotnet-tools-internal are added if dotnet-eng and dotnet-tools are present. +# In addition, this script also enables disabled internal Maestro (darc-int*) feeds. # # Optionally, this script also adds a credential entry for each of the internal feeds if supplied. # @@ -173,6 +174,18 @@ for DotNetVersion in ${DotNetVersions[@]} ; do fi done +# Check for dotnet-eng and add dotnet-eng-internal if present +grep -i " /dev/null +if [ "$?" == "0" ]; then + AddOrEnablePackageSource "dotnet-eng-internal" "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-eng-internal/nuget/$FeedSuffix" +fi + +# Check for dotnet-tools and add dotnet-tools-internal if present +grep -i " /dev/null +if [ "$?" == "0" ]; then + AddOrEnablePackageSource "dotnet-tools-internal" "https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-tools-internal/nuget/$FeedSuffix" +fi + # I want things split line by line PrevIFS=$IFS IFS=$'\n' diff --git a/eng/common/build.sh b/eng/common/build.sh index ec3e80d189ea..9767bb411a4f 100755 --- a/eng/common/build.sh +++ b/eng/common/build.sh @@ -92,7 +92,7 @@ runtime_source_feed='' runtime_source_feed_key='' properties=() -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "${1/#--/-}" | tr "[:upper:]" "[:lower:]")" case "$opt" in -help|-h) diff --git a/eng/common/core-templates/job/job.yml b/eng/common/core-templates/job/job.yml index 748c4f07a64d..5ce518406198 100644 --- a/eng/common/core-templates/job/job.yml +++ b/eng/common/core-templates/job/job.yml @@ -19,8 +19,6 @@ parameters: # publishing defaults artifacts: '' enableMicrobuild: false - enablePreviewMicrobuild: false - microbuildPluginVersion: 'latest' enableMicrobuildForMacAndLinux: false microbuildUseESRP: true enablePublishBuildArtifacts: false @@ -73,8 +71,6 @@ jobs: templateContext: ${{ parameters.templateContext }} variables: - - name: AllowPtrToDetectTestRunRetryFiles - value: true - ${{ if ne(parameters.enableTelemetry, 'false') }}: - name: DOTNET_CLI_TELEMETRY_PROFILE value: '$(Build.Repository.Uri)' @@ -132,8 +128,6 @@ jobs: - template: /eng/common/core-templates/steps/install-microbuild.yml parameters: enableMicrobuild: ${{ parameters.enableMicrobuild }} - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }} enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} microbuildUseESRP: ${{ parameters.microbuildUseESRP }} continueOnError: ${{ parameters.continueOnError }} @@ -159,8 +153,6 @@ jobs: - template: /eng/common/core-templates/steps/cleanup-microbuild.yml parameters: enableMicrobuild: ${{ parameters.enableMicrobuild }} - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildPluginVersion: ${{ parameters.microbuildPluginVersion }} enableMicrobuildForMacAndLinux: ${{ parameters.enableMicrobuildForMacAndLinux }} continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/core-templates/job/onelocbuild.yml b/eng/common/core-templates/job/onelocbuild.yml index c5788829a872..eefed3b667a4 100644 --- a/eng/common/core-templates/job/onelocbuild.yml +++ b/eng/common/core-templates/job/onelocbuild.yml @@ -52,13 +52,13 @@ jobs: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO - image: 1ESPT-Windows2022 + image: 1ESPT-Windows2025 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: name: $(DncEngInternalBuildPool) - image: 1es-windows-2022 + image: windows.vs2026.amd64 os: windows steps: diff --git a/eng/common/core-templates/job/publish-build-assets.yml b/eng/common/core-templates/job/publish-build-assets.yml index 8b5c635fe807..9afcb8ae1590 100644 --- a/eng/common/core-templates/job/publish-build-assets.yml +++ b/eng/common/core-templates/job/publish-build-assets.yml @@ -74,13 +74,13 @@ jobs: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO - image: 1ESPT-Windows2022 + image: 1ESPT-Windows2025 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ if ne(variables['System.TeamProject'], 'DevDiv') }}: name: NetCore1ESPool-Publishing-Internal - image: windows.vs2019.amd64 + image: windows.vs2026.amd64 os: windows steps: - ${{ if eq(parameters.is1ESPipeline, '') }}: @@ -91,8 +91,8 @@ jobs: fetchDepth: 3 clean: true - - ${{ if eq(parameters.isAssetlessBuild, 'false') }}: - - ${{ if eq(parameters.publishingVersion, 3) }}: + - ${{ if eq(parameters.isAssetlessBuild, 'false') }}: + - ${{ if eq(parameters.publishingVersion, 3) }}: - task: DownloadPipelineArtifact@2 displayName: Download Asset Manifests inputs: @@ -117,7 +117,7 @@ jobs: flattenFolders: true condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - + - task: NuGetAuthenticate@1 # Populate internal runtime variables. @@ -125,7 +125,7 @@ jobs: ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: parameters: legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) - + - template: /eng/common/templates/steps/enable-internal-runtimes.yml - task: AzureCLI@2 @@ -145,7 +145,7 @@ jobs: condition: ${{ parameters.condition }} continueOnError: ${{ parameters.continueOnError }} - + - task: powershell@2 displayName: Create ReleaseConfigs Artifact inputs: @@ -173,7 +173,7 @@ jobs: artifactName: AssetManifests displayName: 'Publish Merged Manifest' retryCountOnTaskFailure: 10 # for any logs being locked - sbomEnabled: false # we don't need SBOM for logs + sbomEnabled: false # we don't need SBOM for logs - template: /eng/common/core-templates/steps/publish-build-artifacts.yml parameters: @@ -190,7 +190,7 @@ jobs: BARBuildId: ${{ parameters.BARBuildId }} PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} is1ESPipeline: ${{ parameters.is1ESPipeline }} - + # Darc is targeting 8.0, so make sure it's installed - task: UseDotNet@2 inputs: @@ -218,4 +218,4 @@ jobs: - template: /eng/common/core-templates/steps/publish-logs.yml parameters: is1ESPipeline: ${{ parameters.is1ESPipeline }} - JobLabel: 'Publish_Artifacts_Logs' + JobLabel: 'Publish_Artifacts_Logs' diff --git a/eng/common/core-templates/job/source-build.yml b/eng/common/core-templates/job/source-build.yml index 9d820f974211..1997c2ae00d7 100644 --- a/eng/common/core-templates/job/source-build.yml +++ b/eng/common/core-templates/job/source-build.yml @@ -60,19 +60,19 @@ jobs: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] - demands: ImageOverride -equals build.ubuntu.2204.amd64 + demands: ImageOverride -equals build.azurelinux.3.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] - image: 1es-azurelinux-3 + image: build.azurelinux.3.amd64 os: linux ${{ else }}: pool: ${{ if eq(variables['System.TeamProject'], 'public') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore-Svc-Public' ), False, 'NetCore-Public')] - demands: ImageOverride -equals Build.Ubuntu.2204.Amd64.Open + demands: ImageOverride -equals build.azurelinux.3.amd64.open ${{ if eq(variables['System.TeamProject'], 'internal') }}: name: $[replace(replace(eq(contains(coalesce(variables['System.PullRequest.TargetBranch'], variables['Build.SourceBranch'], 'refs/heads/main'), 'release'), 'true'), True, 'NetCore1ESPool-Svc-Internal'), False, 'NetCore1ESPool-Internal')] - demands: ImageOverride -equals Build.Ubuntu.2204.Amd64 + demands: ImageOverride -equals build.azurelinux.3.amd64 ${{ if ne(parameters.platform.pool, '') }}: pool: ${{ parameters.platform.pool }} diff --git a/eng/common/core-templates/post-build/post-build.yml b/eng/common/core-templates/post-build/post-build.yml index 06864cd1feb8..2df4acb76859 100644 --- a/eng/common/core-templates/post-build/post-build.yml +++ b/eng/common/core-templates/post-build/post-build.yml @@ -1,106 +1,106 @@ parameters: -# Which publishing infra should be used. THIS SHOULD MATCH THE VERSION ON THE BUILD MANIFEST. -# Publishing V1 is no longer supported -# Publishing V2 is no longer supported -# Publishing V3 is the default -- name: publishingInfraVersion - displayName: Which version of publishing should be used to promote the build definition? - type: number - default: 3 - values: - - 3 - -- name: BARBuildId - displayName: BAR Build Id - type: number - default: 0 - -- name: PromoteToChannelIds - displayName: Channel to promote BARBuildId to - type: string - default: '' - -- name: enableSourceLinkValidation - displayName: Enable SourceLink validation - type: boolean - default: false - -- name: enableSigningValidation - displayName: Enable signing validation - type: boolean - default: true - -- name: enableSymbolValidation - displayName: Enable symbol validation - type: boolean - default: false - -- name: enableNugetValidation - displayName: Enable NuGet validation - type: boolean - default: true - -- name: publishInstallersAndChecksums - displayName: Publish installers and checksums - type: boolean - default: true - -- name: requireDefaultChannels - displayName: Fail the build if there are no default channel(s) registrations for the current build - type: boolean - default: false - -- name: SDLValidationParameters - type: object - default: - enable: false - publishGdn: false - continueOnError: false - params: '' - artifactNames: '' - downloadArtifacts: true - -- name: isAssetlessBuild - type: boolean - displayName: Is Assetless Build - default: false - -# These parameters let the user customize the call to sdk-task.ps1 for publishing -# symbols & general artifacts as well as for signing validation -- name: symbolPublishingAdditionalParameters - displayName: Symbol publishing additional parameters - type: string - default: '' - -- name: artifactsPublishingAdditionalParameters - displayName: Artifact publishing additional parameters - type: string - default: '' - -- name: signingValidationAdditionalParameters - displayName: Signing validation additional parameters - type: string - default: '' - -# Which stages should finish execution before post-build stages start -- name: validateDependsOn - type: object - default: - - build - -- name: publishDependsOn - type: object - default: - - Validate - -# Optional: Call asset publishing rather than running in a separate stage -- name: publishAssetsImmediately - type: boolean - default: false - -- name: is1ESPipeline - type: boolean - default: false + # Which publishing infra should be used. THIS SHOULD MATCH THE VERSION ON THE BUILD MANIFEST. + # Publishing V1 is no longer supported + # Publishing V2 is no longer supported + # Publishing V3 is the default + - name: publishingInfraVersion + displayName: Which version of publishing should be used to promote the build definition? + type: number + default: 3 + values: + - 3 + + - name: BARBuildId + displayName: BAR Build Id + type: number + default: 0 + + - name: PromoteToChannelIds + displayName: Channel to promote BARBuildId to + type: string + default: '' + + - name: enableSourceLinkValidation + displayName: Enable SourceLink validation + type: boolean + default: false + + - name: enableSigningValidation + displayName: Enable signing validation + type: boolean + default: true + + - name: enableSymbolValidation + displayName: Enable symbol validation + type: boolean + default: false + + - name: enableNugetValidation + displayName: Enable NuGet validation + type: boolean + default: true + + - name: publishInstallersAndChecksums + displayName: Publish installers and checksums + type: boolean + default: true + + - name: requireDefaultChannels + displayName: Fail the build if there are no default channel(s) registrations for the current build + type: boolean + default: false + + - name: SDLValidationParameters + type: object + default: + enable: false + publishGdn: false + continueOnError: false + params: '' + artifactNames: '' + downloadArtifacts: true + + - name: isAssetlessBuild + type: boolean + displayName: Is Assetless Build + default: false + + # These parameters let the user customize the call to sdk-task.ps1 for publishing + # symbols & general artifacts as well as for signing validation + - name: symbolPublishingAdditionalParameters + displayName: Symbol publishing additional parameters + type: string + default: '' + + - name: artifactsPublishingAdditionalParameters + displayName: Artifact publishing additional parameters + type: string + default: '' + + - name: signingValidationAdditionalParameters + displayName: Signing validation additional parameters + type: string + default: '' + + # Which stages should finish execution before post-build stages start + - name: validateDependsOn + type: object + default: + - build + + - name: publishDependsOn + type: object + default: + - Validate + + # Optional: Call asset publishing rather than running in a separate stage + - name: publishAssetsImmediately + type: boolean + default: false + + - name: is1ESPipeline + type: boolean + default: false stages: - ${{ if or(eq( parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}: @@ -108,10 +108,10 @@ stages: dependsOn: ${{ parameters.validateDependsOn }} displayName: Validate Build Assets variables: - - template: /eng/common/core-templates/post-build/common-variables.yml - - template: /eng/common/core-templates/variables/pool-providers.yml - parameters: - is1ESPipeline: ${{ parameters.is1ESPipeline }} + - template: /eng/common/core-templates/post-build/common-variables.yml + - template: /eng/common/core-templates/variables/pool-providers.yml + parameters: + is1ESPipeline: ${{ parameters.is1ESPipeline }} jobs: - job: displayName: NuGet Validation @@ -120,7 +120,7 @@ stages: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO - image: 1ESPT-Windows2022 + image: 1ESPT-Windows2025 demands: Cmd os: windows # If it's not devdiv, it's dnceng @@ -134,28 +134,28 @@ stages: demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml - parameters: - BARBuildId: ${{ parameters.BARBuildId }} - PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} - is1ESPipeline: ${{ parameters.is1ESPipeline }} - - - task: DownloadBuildArtifacts@0 - displayName: Download Package Artifacts - inputs: - buildType: specific - buildVersionToDownload: specific - project: $(AzDOProjectName) - pipeline: $(AzDOPipelineId) - buildId: $(AzDOBuildId) - artifactName: PackageArtifacts - checkDownloadedFiles: true - - - task: PowerShell@2 - displayName: Validate - inputs: - filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/nuget-validation.ps1 - arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ + - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + is1ESPipeline: ${{ parameters.is1ESPipeline }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: PackageArtifacts + checkDownloadedFiles: true + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/nuget-validation.ps1 + arguments: -PackagesPath $(Build.ArtifactStagingDirectory)/PackageArtifacts/ - job: displayName: Signing Validation @@ -164,59 +164,59 @@ stages: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO - image: 1ESPT-Windows2022 + image: 1ESPT-Windows2025 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ else }}: - ${{ if eq(parameters.is1ESPipeline, true) }}: + ${{ if eq(parameters.is1ESPipeline, true) }}: name: $(DncEngInternalBuildPool) - image: 1es-windows-2022 + image: windows.vs2026.amd64 os: windows ${{ else }}: name: $(DncEngInternalBuildPool) demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml - parameters: - BARBuildId: ${{ parameters.BARBuildId }} - PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} - is1ESPipeline: ${{ parameters.is1ESPipeline }} - - - task: DownloadBuildArtifacts@0 - displayName: Download Package Artifacts - inputs: - buildType: specific - buildVersionToDownload: specific - project: $(AzDOProjectName) - pipeline: $(AzDOPipelineId) - buildId: $(AzDOBuildId) - artifactName: PackageArtifacts - checkDownloadedFiles: true - - # This is necessary whenever we want to publish/restore to an AzDO private feed - # Since sdk-task.ps1 tries to restore packages we need to do this authentication here - # otherwise it'll complain about accessing a private feed. - - task: NuGetAuthenticate@1 - displayName: 'Authenticate to AzDO Feeds' - - # Signing validation will optionally work with the buildmanifest file which is downloaded from - # Azure DevOps above. - - task: PowerShell@2 - displayName: Validate - inputs: - filePath: eng\common\sdk-task.ps1 - arguments: -task SigningValidation -restore -msbuildEngine vs - /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts' - /p:SignCheckExclusionsFile='$(System.DefaultWorkingDirectory)/eng/SignCheckExclusionsFile.txt' - ${{ parameters.signingValidationAdditionalParameters }} - - - template: /eng/common/core-templates/steps/publish-logs.yml - parameters: - is1ESPipeline: ${{ parameters.is1ESPipeline }} - StageLabel: 'Validation' - JobLabel: 'Signing' - BinlogToolVersion: $(BinlogToolVersion) + - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + is1ESPipeline: ${{ parameters.is1ESPipeline }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Package Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: PackageArtifacts + checkDownloadedFiles: true + + # This is necessary whenever we want to publish/restore to an AzDO private feed + # Since sdk-task.ps1 tries to restore packages we need to do this authentication here + # otherwise it'll complain about accessing a private feed. + - task: NuGetAuthenticate@1 + displayName: 'Authenticate to AzDO Feeds' + + # Signing validation will optionally work with the buildmanifest file which is downloaded from + # Azure DevOps above. + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: eng\common\sdk-task.ps1 + arguments: -task SigningValidation -restore -msbuildEngine vs + /p:PackageBasePath='$(Build.ArtifactStagingDirectory)/PackageArtifacts' + /p:SignCheckExclusionsFile='$(System.DefaultWorkingDirectory)/eng/SignCheckExclusionsFile.txt' + ${{ parameters.signingValidationAdditionalParameters }} + + - template: /eng/common/core-templates/steps/publish-logs.yml + parameters: + is1ESPipeline: ${{ parameters.is1ESPipeline }} + StageLabel: 'Validation' + JobLabel: 'Signing' + BinlogToolVersion: $(BinlogToolVersion) - job: displayName: SourceLink Validation @@ -225,46 +225,46 @@ stages: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO - image: 1ESPT-Windows2022 + image: 1ESPT-Windows2025 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ else }}: - ${{ if eq(parameters.is1ESPipeline, true) }}: + ${{ if eq(parameters.is1ESPipeline, true) }}: name: $(DncEngInternalBuildPool) - image: 1es-windows-2022 + image: windows.vs2026.amd64 os: windows ${{ else }}: name: $(DncEngInternalBuildPool) demands: ImageOverride -equals windows.vs2026preview.scout.amd64 steps: - - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml - parameters: - BARBuildId: ${{ parameters.BARBuildId }} - PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} - is1ESPipeline: ${{ parameters.is1ESPipeline }} - - - task: DownloadBuildArtifacts@0 - displayName: Download Blob Artifacts - inputs: - buildType: specific - buildVersionToDownload: specific - project: $(AzDOProjectName) - pipeline: $(AzDOPipelineId) - buildId: $(AzDOBuildId) - artifactName: BlobArtifacts - checkDownloadedFiles: true - - - task: PowerShell@2 - displayName: Validate - inputs: - filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/sourcelink-validation.ps1 - arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ - -ExtractPath $(Agent.BuildDirectory)/Extract/ - -GHRepoName $(Build.Repository.Name) - -GHCommit $(Build.SourceVersion) - -SourcelinkCliVersion $(SourceLinkCLIVersion) - continueOnError: true + - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + is1ESPipeline: ${{ parameters.is1ESPipeline }} + + - task: DownloadBuildArtifacts@0 + displayName: Download Blob Artifacts + inputs: + buildType: specific + buildVersionToDownload: specific + project: $(AzDOProjectName) + pipeline: $(AzDOPipelineId) + buildId: $(AzDOBuildId) + artifactName: BlobArtifacts + checkDownloadedFiles: true + + - task: PowerShell@2 + displayName: Validate + inputs: + filePath: $(System.DefaultWorkingDirectory)/eng/common/post-build/sourcelink-validation.ps1 + arguments: -InputPath $(Build.ArtifactStagingDirectory)/BlobArtifacts/ + -ExtractPath $(Agent.BuildDirectory)/Extract/ + -GHRepoName $(Build.Repository.Name) + -GHCommit $(Build.SourceVersion) + -SourcelinkCliVersion $(SourceLinkCLIVersion) + continueOnError: true - ${{ if ne(parameters.publishAssetsImmediately, 'true') }}: - stage: publish_using_darc @@ -274,10 +274,10 @@ stages: dependsOn: ${{ parameters.validateDependsOn }} displayName: Publish using Darc variables: - - template: /eng/common/core-templates/post-build/common-variables.yml - - template: /eng/common/core-templates/variables/pool-providers.yml - parameters: - is1ESPipeline: ${{ parameters.is1ESPipeline }} + - template: /eng/common/core-templates/post-build/common-variables.yml + - template: /eng/common/core-templates/variables/pool-providers.yml + parameters: + is1ESPipeline: ${{ parameters.is1ESPipeline }} jobs: - job: displayName: Publish Using Darc @@ -286,46 +286,47 @@ stages: # We don't use the collection uri here because it might vary (.visualstudio.com vs. dev.azure.com) ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: name: AzurePipelines-EO - image: 1ESPT-Windows2022 + image: 1ESPT-Windows2025 demands: Cmd os: windows # If it's not devdiv, it's dnceng ${{ else }}: - ${{ if eq(parameters.is1ESPipeline, true) }}: + ${{ if eq(parameters.is1ESPipeline, true) }}: name: NetCore1ESPool-Publishing-Internal - image: windows.vs2019.amd64 + image: windows.vs2026.amd64 os: windows ${{ else }}: name: NetCore1ESPool-Publishing-Internal - demands: ImageOverride -equals windows.vs2019.amd64 + demands: ImageOverride -equals windows.vs2026.amd64 steps: - - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml - parameters: - BARBuildId: ${{ parameters.BARBuildId }} - PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} - is1ESPipeline: ${{ parameters.is1ESPipeline }} - - - task: NuGetAuthenticate@1 - - # Populate internal runtime variables. - - template: /eng/common/templates/steps/enable-internal-sources.yml - parameters: - legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) - - - template: /eng/common/templates/steps/enable-internal-runtimes.yml - - - task: UseDotNet@2 - inputs: - version: 8.0.x - - - task: AzureCLI@2 - displayName: Publish Using Darc - inputs: - azureSubscription: "Darc: Maestro Production" - scriptType: ps - scriptLocation: scriptPath - scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1 - arguments: > + - template: /eng/common/core-templates/post-build/setup-maestro-vars.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + is1ESPipeline: ${{ parameters.is1ESPipeline }} + + - task: NuGetAuthenticate@1 + + # Populate internal runtime variables. + - template: /eng/common/templates/steps/enable-internal-sources.yml + parameters: + legacyCredential: $(dn-bot-dnceng-artifact-feeds-rw) + + - template: /eng/common/templates/steps/enable-internal-runtimes.yml + + # Darc is targeting 8.0, so make sure it's installed + - task: UseDotNet@2 + inputs: + version: 8.0.x + + - task: AzureCLI@2 + displayName: Publish Using Darc + inputs: + azureSubscription: "Darc: Maestro Production" + scriptType: ps + scriptLocation: scriptPath + scriptPath: $(System.DefaultWorkingDirectory)/eng/common/post-build/publish-using-darc.ps1 + arguments: > -BuildId $(BARBuildId) -PublishingInfraVersion ${{ parameters.publishingInfraVersion }} -AzdoToken '$(System.AccessToken)' diff --git a/eng/common/core-templates/steps/generate-sbom.yml b/eng/common/core-templates/steps/generate-sbom.yml index 003f7eae0fa5..c05f65027979 100644 --- a/eng/common/core-templates/steps/generate-sbom.yml +++ b/eng/common/core-templates/steps/generate-sbom.yml @@ -5,7 +5,7 @@ # IgnoreDirectories - Directories to ignore for SBOM generation. This will be passed through to the CG component detector. parameters: - PackageVersion: 11.0.0 + PackageVersion: 10.0.0 BuildDropPath: '$(System.DefaultWorkingDirectory)/artifacts' PackageName: '.NET' ManifestDirPath: $(Build.ArtifactStagingDirectory)/sbom diff --git a/eng/common/core-templates/steps/install-microbuild-impl.yml b/eng/common/core-templates/steps/install-microbuild-impl.yml deleted file mode 100644 index da22beb3f60c..000000000000 --- a/eng/common/core-templates/steps/install-microbuild-impl.yml +++ /dev/null @@ -1,34 +0,0 @@ -parameters: - - name: microbuildTaskInputs - type: object - default: {} - - - name: microbuildEnv - type: object - default: {} - - - name: enablePreviewMicrobuild - type: boolean - default: false - - - name: condition - type: string - - - name: continueOnError - type: boolean - -steps: -- ${{ if eq(parameters.enablePreviewMicrobuild, true) }}: - - task: MicroBuildSigningPluginPreview@4 - displayName: Install Preview MicroBuild plugin - inputs: ${{ parameters.microbuildTaskInputs }} - env: ${{ parameters.microbuildEnv }} - continueOnError: ${{ parameters.continueOnError }} - condition: ${{ parameters.condition }} -- ${{ else }}: - - task: MicroBuildSigningPlugin@4 - displayName: Install MicroBuild plugin - inputs: ${{ parameters.microbuildTaskInputs }} - env: ${{ parameters.microbuildEnv }} - continueOnError: ${{ parameters.continueOnError }} - condition: ${{ parameters.condition }} diff --git a/eng/common/core-templates/steps/install-microbuild.yml b/eng/common/core-templates/steps/install-microbuild.yml index 4f4b56ed2a6b..553fce66b940 100644 --- a/eng/common/core-templates/steps/install-microbuild.yml +++ b/eng/common/core-templates/steps/install-microbuild.yml @@ -4,8 +4,6 @@ parameters: # Enable install tasks for MicroBuild on Mac and Linux # Will be ignored if 'enableMicrobuild' is false or 'Agent.Os' is 'Windows_NT' enableMicrobuildForMacAndLinux: false - # Enable preview version of MB signing plugin - enablePreviewMicrobuild: false # Determines whether the ESRP service connection information should be passed to the signing plugin. # This overlaps with _SignType to some degree. We only need the service connection for real signing. # It's important that the service connection not be passed to the MicroBuildSigningPlugin task in this place. @@ -15,8 +13,6 @@ parameters: microbuildUseESRP: true # Microbuild installation directory microBuildOutputFolder: $(Agent.TempDirectory)/MicroBuild - # Microbuild version - microbuildPluginVersion: 'latest' continueOnError: false @@ -73,46 +69,42 @@ steps: # YAML expansion, and Windows vs. Linux/Mac uses different service connections. However, # we can avoid including the MB install step if not enabled at all. This avoids a bunch of # extra pipeline authorizations, since most pipelines do not sign on non-Windows. - - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self - parameters: - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildTaskInputs: + - task: MicroBuildSigningPlugin@4 + displayName: Install MicroBuild plugin (Windows) + inputs: + signType: $(_SignType) + zipSources: false + feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json + ${{ if eq(parameters.microbuildUseESRP, true) }}: + ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' + ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: + ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ${{ else }}: + ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca + env: + TeamName: $(_TeamName) + MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + continueOnError: ${{ parameters.continueOnError }} + condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) + + - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}: + - task: MicroBuildSigningPlugin@4 + displayName: Install MicroBuild plugin (non-Windows) + inputs: signType: $(_SignType) zipSources: false feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - version: ${{ parameters.microbuildPluginVersion }} + workingDirectory: ${{ parameters.microBuildOutputFolder }} ${{ if eq(parameters.microbuildUseESRP, true) }}: ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: 6cc74545-d7b9-4050-9dfa-ebefcc8961ea + ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 ${{ else }}: - ConnectedPMEServiceName: 248d384a-b39b-46e3-8ad5-c2c210d5e7ca - microbuildEnv: + ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc + env: TeamName: $(_TeamName) MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} SYSTEM_ACCESSTOKEN: $(System.AccessToken) continueOnError: ${{ parameters.continueOnError }} - condition: and(succeeded(), eq(variables['Agent.Os'], 'Windows_NT'), in(variables['_SignType'], 'real', 'test')) - - - ${{ if eq(parameters.enableMicrobuildForMacAndLinux, true) }}: - - template: /eng/common/core-templates/steps/install-microbuild-impl.yml@self - parameters: - enablePreviewMicrobuild: ${{ parameters.enablePreviewMicrobuild }} - microbuildTaskInputs: - signType: $(_SignType) - zipSources: false - feedSource: https://dnceng.pkgs.visualstudio.com/_packaging/MicroBuildToolset/nuget/v3/index.json - version: ${{ parameters.microbuildPluginVersion }} - workingDirectory: ${{ parameters.microBuildOutputFolder }} - ${{ if eq(parameters.microbuildUseESRP, true) }}: - ConnectedServiceName: 'MicroBuild Signing Task (DevDiv)' - ${{ if eq(variables['System.TeamProject'], 'DevDiv') }}: - ConnectedPMEServiceName: beb8cb23-b303-4c95-ab26-9e44bc958d39 - ${{ else }}: - ConnectedPMEServiceName: c24de2a5-cc7a-493d-95e4-8e5ff5cad2bc - microbuildEnv: - TeamName: $(_TeamName) - MicroBuildOutputFolderOverride: ${{ parameters.microBuildOutputFolder }} - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - continueOnError: ${{ parameters.continueOnError }} - condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) + condition: and(succeeded(), ne(variables['Agent.Os'], 'Windows_NT'), eq(variables['_SignType'], 'real')) diff --git a/eng/common/core-templates/steps/publish-logs.yml b/eng/common/core-templates/steps/publish-logs.yml index 5a927b4c7bcb..a9ea99ba6aaa 100644 --- a/eng/common/core-templates/steps/publish-logs.yml +++ b/eng/common/core-templates/steps/publish-logs.yml @@ -31,7 +31,6 @@ steps: -runtimeSourceFeed https://ci.dot.net/internal -runtimeSourceFeedKey '$(dotnetbuilds-internal-container-read-token-base64)' '$(publishing-dnceng-devdiv-code-r-build-re)' - '$(MaestroAccessToken)' '$(dn-bot-all-orgs-artifact-feeds-rw)' '$(akams-client-id)' '$(microsoft-symbol-server-pat)' diff --git a/eng/common/core-templates/steps/source-build.yml b/eng/common/core-templates/steps/source-build.yml index acf16ed34963..b9c86c18ae42 100644 --- a/eng/common/core-templates/steps/source-build.yml +++ b/eng/common/core-templates/steps/source-build.yml @@ -24,7 +24,7 @@ steps: # in the default public locations. internalRuntimeDownloadArgs= if [ '$(dotnetbuilds-internal-container-read-token-base64)' != '$''(dotnetbuilds-internal-container-read-token-base64)' ]; then - internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey $(dotnetbuilds-internal-container-read-token-base64)' + internalRuntimeDownloadArgs='/p:DotNetRuntimeSourceFeed=https://ci.dot.net/internal /p:DotNetRuntimeSourceFeedKey=$(dotnetbuilds-internal-container-read-token-base64) --runtimesourcefeed https://ci.dot.net/internal --runtimesourcefeedkey '$(dotnetbuilds-internal-container-read-token-base64)'' fi buildConfig=Release diff --git a/eng/common/core-templates/steps/source-index-stage1-publish.yml b/eng/common/core-templates/steps/source-index-stage1-publish.yml index 3ad83b8c3075..e9a694afa58e 100644 --- a/eng/common/core-templates/steps/source-index-stage1-publish.yml +++ b/eng/common/core-templates/steps/source-index-stage1-publish.yml @@ -1,6 +1,6 @@ parameters: - sourceIndexUploadPackageVersion: 2.0.0-20250906.1 - sourceIndexProcessBinlogPackageVersion: 1.0.1-20250906.1 + sourceIndexUploadPackageVersion: 2.0.0-20250818.1 + sourceIndexProcessBinlogPackageVersion: 1.0.1-20250818.1 sourceIndexPackageSource: https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json binlogPath: artifacts/log/Debug/Build.binlog @@ -14,8 +14,8 @@ steps: workingDirectory: $(Agent.TempDirectory) - script: | - $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --source ${{parameters.sourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools - $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --source ${{parameters.sourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install BinLogToSln --version ${{parameters.sourceIndexProcessBinlogPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools + $(Agent.TempDirectory)/dotnet/dotnet tool install UploadIndexStage1 --version ${{parameters.sourceIndexUploadPackageVersion}} --add-source ${{parameters.SourceIndexPackageSource}} --tool-path $(Agent.TempDirectory)/.source-index/tools displayName: "Source Index: Download netsourceindex Tools" # Set working directory to temp directory so 'dotnet' doesn't try to use global.json and use the repo's sdk. workingDirectory: $(Agent.TempDirectory) diff --git a/eng/common/cross/build-rootfs.sh b/eng/common/cross/build-rootfs.sh index 9b7eede4e50f..8abfb71f7275 100755 --- a/eng/common/cross/build-rootfs.sh +++ b/eng/common/cross/build-rootfs.sh @@ -72,7 +72,7 @@ __AlpinePackages+=" krb5-dev" __AlpinePackages+=" openssl-dev" __AlpinePackages+=" zlib-dev" -__FreeBSDBase="13.5-RELEASE" +__FreeBSDBase="13.4-RELEASE" __FreeBSDPkg="1.21.3" __FreeBSDABI="13" __FreeBSDPackages="libunwind" @@ -383,7 +383,7 @@ while :; do ;; freebsd14) __CodeName=freebsd - __FreeBSDBase="14.3-RELEASE" + __FreeBSDBase="14.2-RELEASE" __FreeBSDABI="14" __SkipUnmount=1 ;; diff --git a/eng/common/darc-init.sh b/eng/common/darc-init.sh index 9f5ad6b763b5..e889f439b8dc 100755 --- a/eng/common/darc-init.sh +++ b/eng/common/darc-init.sh @@ -5,7 +5,7 @@ darcVersion='' versionEndpoint='https://maestro.dot.net/api/assets/darc-version?api-version=2020-02-20' verbosity='minimal' -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in --darcversion) diff --git a/eng/common/dotnet-install.sh b/eng/common/dotnet-install.sh index 61f302bb6775..7b9d97e3bd4d 100755 --- a/eng/common/dotnet-install.sh +++ b/eng/common/dotnet-install.sh @@ -18,7 +18,7 @@ architecture='' runtime='dotnet' runtimeSourceFeed='' runtimeSourceFeedKey='' -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in -version|-v) diff --git a/eng/common/dotnet.sh b/eng/common/dotnet.sh index f6d24871c1d4..2ef68235675f 100644 --- a/eng/common/dotnet.sh +++ b/eng/common/dotnet.sh @@ -19,7 +19,7 @@ source $scriptroot/tools.sh InitializeDotNetCli true # install # Invoke acquired SDK with args if they are provided -if [[ $# -gt 0 ]]; then +if [[ $# > 0 ]]; then __dotnetDir=${_InitializeDotNetCli} dotnetPath=${__dotnetDir}/dotnet ${dotnetPath} "$@" diff --git a/eng/common/internal-feed-operations.sh b/eng/common/internal-feed-operations.sh index 6299e7effd4c..9378223ba095 100755 --- a/eng/common/internal-feed-operations.sh +++ b/eng/common/internal-feed-operations.sh @@ -100,7 +100,7 @@ operation='' authToken='' repoName='' -while [[ $# -gt 0 ]]; do +while [[ $# > 0 ]]; do opt="$(echo "$1" | tr "[:upper:]" "[:lower:]")" case "$opt" in --operation) diff --git a/eng/common/native/install-dependencies.sh b/eng/common/native/install-dependencies.sh index 11f81cbd40d4..477a44f335be 100644 --- a/eng/common/native/install-dependencies.sh +++ b/eng/common/native/install-dependencies.sh @@ -27,11 +27,9 @@ case "$os" in libssl-dev libkrb5-dev pigz cpio localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 - elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ] || [ "$ID" = "centos" ]; then + elif [ "$ID" = "fedora" ] || [ "$ID" = "rhel" ] || [ "$ID" = "azurelinux" ]; then pkg_mgr="$(command -v tdnf 2>/dev/null || command -v dnf)" $pkg_mgr install -y cmake llvm lld lldb clang python curl libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio - elif [ "$ID" = "amzn" ]; then - dnf install -y cmake llvm lld lldb clang python libicu-devel openssl-devel krb5-devel lttng-ust-devel pigz cpio elif [ "$ID" = "alpine" ]; then apk add build-base cmake bash curl clang llvm-dev lld lldb krb5-dev lttng-ust-dev icu-dev openssl-dev pigz cpio else diff --git a/eng/common/post-build/redact-logs.ps1 b/eng/common/post-build/redact-logs.ps1 index fc0218a013d1..472d5bb562c9 100644 --- a/eng/common/post-build/redact-logs.ps1 +++ b/eng/common/post-build/redact-logs.ps1 @@ -9,8 +9,7 @@ param( [Parameter(Mandatory=$false)][string] $TokensFilePath, [Parameter(ValueFromRemainingArguments=$true)][String[]]$TokensToRedact, [Parameter(Mandatory=$false)][string] $runtimeSourceFeed, - [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey -) + [Parameter(Mandatory=$false)][string] $runtimeSourceFeedKey) try { $ErrorActionPreference = 'Stop' diff --git a/eng/common/templates-official/variables/pool-providers.yml b/eng/common/templates-official/variables/pool-providers.yml index 1f308b24efc4..2cc3ae305d5a 100644 --- a/eng/common/templates-official/variables/pool-providers.yml +++ b/eng/common/templates-official/variables/pool-providers.yml @@ -23,7 +23,7 @@ # # pool: # name: $(DncEngInternalBuildPool) -# image: 1es-windows-2022 +# image: windows.vs2026.amd64 variables: # Coalesce the target and source branches so we know when a PR targets a release branch diff --git a/eng/common/templates/steps/vmr-sync.yml b/eng/common/templates/steps/vmr-sync.yml index 599afb6186b8..eb619c502683 100644 --- a/eng/common/templates/steps/vmr-sync.yml +++ b/eng/common/templates/steps/vmr-sync.yml @@ -38,27 +38,6 @@ steps: displayName: Label PR commit workingDirectory: $(Agent.BuildDirectory)/repo -- script: | - vmr_sha=$(grep -oP '(?<=Sha=")[^"]*' $(Agent.BuildDirectory)/repo/eng/Version.Details.xml) - echo "##vso[task.setvariable variable=vmr_sha]$vmr_sha" - displayName: Obtain the vmr sha from Version.Details.xml (Unix) - condition: ne(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo - -- powershell: | - [xml]$xml = Get-Content -Path $(Agent.BuildDirectory)/repo/eng/Version.Details.xml - $vmr_sha = $xml.SelectSingleNode("//Source").Sha - Write-Output "##vso[task.setvariable variable=vmr_sha]$vmr_sha" - displayName: Obtain the vmr sha from Version.Details.xml (Windows) - condition: eq(variables['Agent.OS'], 'Windows_NT') - workingDirectory: $(Agent.BuildDirectory)/repo - -- script: | - git fetch --all - git checkout $(vmr_sha) - displayName: Checkout VMR at correct sha for repo flow - workingDirectory: ${{ parameters.vmrPath }} - - script: | git config --global user.name "dotnet-maestro[bot]" git config --global user.email "dotnet-maestro[bot]@users.noreply.github.com" diff --git a/eng/common/templates/variables/pool-providers.yml b/eng/common/templates/variables/pool-providers.yml index e0b19c14a073..587770f0add4 100644 --- a/eng/common/templates/variables/pool-providers.yml +++ b/eng/common/templates/variables/pool-providers.yml @@ -23,7 +23,7 @@ # # pool: # name: $(DncEngInternalBuildPool) -# demands: ImageOverride -equals windows.vs2019.amd64 +# demands: ImageOverride -equals windows.vs2026.amd64 variables: - ${{ if eq(variables['System.TeamProject'], 'internal') }}: - template: /eng/common/templates-official/variables/pool-providers.yml diff --git a/eng/common/templates/vmr-build-pr.yml b/eng/common/templates/vmr-build-pr.yml index ce3c29a62faf..2f3694fa1323 100644 --- a/eng/common/templates/vmr-build-pr.yml +++ b/eng/common/templates/vmr-build-pr.yml @@ -34,6 +34,7 @@ resources: type: github name: dotnet/dotnet endpoint: dotnet + ref: refs/heads/main # Set to whatever VMR branch the PR build should insert into stages: - template: /eng/pipelines/templates/stages/vmr-build.yml@vmr diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index f6bde2683794..977a2d4b1039 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -157,6 +157,9 @@ function InitializeDotNetCli([bool]$install, [bool]$createSdkLocationFile) { return $global:_DotNetInstallDir } + # Don't resolve runtime, shared framework, or SDK from other locations to ensure build determinism + $env:DOTNET_MULTILEVEL_LOOKUP=0 + # Disable first run since we do not need all ASP.NET packages restored. $env:DOTNET_NOLOGO=1 @@ -222,6 +225,7 @@ function InitializeDotNetCli([bool]$install, [bool]$createSdkLocationFile) { # Make Sure that our bootstrapped dotnet cli is available in future steps of the Azure Pipelines build Write-PipelinePrependPath -Path $dotnetRoot + Write-PipelineSetVariable -Name 'DOTNET_MULTILEVEL_LOOKUP' -Value '0' Write-PipelineSetVariable -Name 'DOTNET_NOLOGO' -Value '1' return $global:_DotNetInstallDir = $dotnetRoot @@ -820,6 +824,11 @@ function MSBuild-Core() { $cmdArgs = "$($buildTool.Command) /m /nologo /clp:Summary /v:$verbosity /nr:$nodeReuse /p:ContinuousIntegrationBuild=$ci" + # Add -mt flag for MSBuild multithreaded mode if enabled via environment variable + if ($env:MSBUILD_MT_ENABLED -eq "1") { + $cmdArgs += ' -mt' + } + if ($warnAsError) { $cmdArgs += ' /warnaserror /p:TreatWarningsAsErrors=true' } diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 6c121300ac7d..1b296f646c23 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -115,6 +115,9 @@ function InitializeDotNetCli { local install=$1 + # Don't resolve runtime, shared framework, or SDK from other locations to ensure build determinism + export DOTNET_MULTILEVEL_LOOKUP=0 + # Disable first run since we want to control all package sources export DOTNET_NOLOGO=1 @@ -163,6 +166,7 @@ function InitializeDotNetCli { # build steps from using anything other than what we've downloaded. Write-PipelinePrependPath -path "$dotnet_root" + Write-PipelineSetVariable -name "DOTNET_MULTILEVEL_LOOKUP" -value "0" Write-PipelineSetVariable -name "DOTNET_NOLOGO" -value "1" # return value @@ -522,7 +526,13 @@ function MSBuild-Core { } } - RunBuildTool "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" + # Add -mt flag for MSBuild multithreaded mode if enabled via environment variable + local mt_switch="" + if [[ "${MSBUILD_MT_ENABLED:-}" == "1" ]]; then + mt_switch="-mt" + fi + + RunBuildTool "$_InitializeBuildToolCommand" /m /nologo /clp:Summary /v:$verbosity /nr:$node_reuse $warnaserror_switch $mt_switch /p:TreatWarningsAsErrors=$warn_as_error /p:ContinuousIntegrationBuild=$ci "$@" } function GetDarc { diff --git a/eng/common/vmr-sync.ps1 b/eng/common/vmr-sync.ps1 index 97302f3205be..b37992d91cf0 100644 --- a/eng/common/vmr-sync.ps1 +++ b/eng/common/vmr-sync.ps1 @@ -103,12 +103,20 @@ Set-StrictMode -Version Latest Highlight 'Installing .NET, preparing the tooling..' . .\eng\common\tools.ps1 $dotnetRoot = InitializeDotNetCli -install:$true +$env:DOTNET_ROOT = $dotnetRoot $darc = Get-Darc -$dotnet = "$dotnetRoot\dotnet.exe" Highlight "Starting the synchronization of VMR.." # Synchronize the VMR +$versionDetailsPath = Resolve-Path (Join-Path $PSScriptRoot '..\Version.Details.xml') | Select-Object -ExpandProperty Path +[xml]$versionDetails = Get-Content -Path $versionDetailsPath +$repoName = $versionDetails.SelectSingleNode('//Source').Mapping +if (-not $repoName) { + Fail "Failed to resolve repo mapping from $versionDetailsPath" + exit 1 +} + $darcArgs = ( "vmr", "forwardflow", "--tmp", $tmpDir, @@ -130,9 +138,27 @@ if ($LASTEXITCODE -eq 0) { Highlight "Synchronization succeeded" } else { - Fail "Synchronization of repo to VMR failed!" - Fail "'$vmrDir' is left in its last state (re-run of this script will reset it)." - Fail "Please inspect the logs which contain path to the failing patch file (use -debugOutput to get all the details)." - Fail "Once you make changes to the conflicting VMR patch, commit it locally and re-run this script." - exit 1 + Highlight "Failed to flow code into the local VMR. Falling back to resetting the VMR to match repo contents..." + git -C $vmrDir reset --hard + + $resetArgs = ( + "vmr", "reset", + "${repoName}:HEAD", + "--vmr", $vmrDir, + "--tmp", $tmpDir, + "--additional-remotes", "${repoName}:${repoRoot}" + ) + + & "$darc" $resetArgs + + if ($LASTEXITCODE -eq 0) { + Highlight "Successfully reset the VMR using 'darc vmr reset'" + } + else { + Fail "Synchronization of repo to VMR failed!" + Fail "'$vmrDir' is left in its last state (re-run of this script will reset it)." + Fail "Please inspect the logs which contain path to the failing patch file (use -debugOutput to get all the details)." + Fail "Once you make changes to the conflicting VMR patch, commit it locally and re-run this script." + exit 1 + } } diff --git a/eng/common/vmr-sync.sh b/eng/common/vmr-sync.sh index 44239e331c0c..198caec59bd4 100644 --- a/eng/common/vmr-sync.sh +++ b/eng/common/vmr-sync.sh @@ -186,6 +186,13 @@ fi # Synchronize the VMR +version_details_path=$(cd "$scriptroot/.."; pwd -P)/Version.Details.xml +repo_name=$(grep -m 1 ' + name.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) || + name.EndsWith(".resx", StringComparison.OrdinalIgnoreCase), + mapRelativePath: static name => name).ConfigureAwait(false); -var packageFiles = Directory.GetFiles(contentFilesDir1, "*", SearchOption.TopDirectoryOnly) - .Concat(Directory.GetFiles(contentFilesDir2, "*", SearchOption.TopDirectoryOnly)) - .Where(f => extensions.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) - .ToList(); -if (packageFiles.Count == 0) throw new InvalidOperationException("No package files found."); + var editorConfigFiles = await GetDirectoryFilesAsync( + httpClient, + sdkCommit, + githubDirectoryPath: "eng", + includeFile: static name => string.Equals(name, "SourcePackage.editorconfig", StringComparison.OrdinalIgnoreCase), + mapRelativePath: static _ => ".editorconfig").ConfigureAwait(false); -var updateSnapshots = string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")); + var xlfFiles = await GetDirectoryFilesAsync( + httpClient, + sdkCommit, + githubDirectoryPath: "src/Cli/Microsoft.DotNet.FileBasedPrograms/xlf", + includeFile: static name => name.EndsWith(".xlf", StringComparison.OrdinalIgnoreCase), + mapRelativePath: static name => $"xlf/{name}").ConfigureAwait(false); -if (updateSnapshots) Directory.CreateDirectory(localSourceDir); + var sourcePackageFiles = sourceFiles + .Concat(editorConfigFiles) + .Concat(xlfFiles) + .ToList(); + if (sourcePackageFiles.Count == 0) throw new InvalidOperationException("No source files found in dotnet/sdk."); -var mismatches = new List(); -foreach (var pkgFile in packageFiles) -{ - var fileName = Path.GetFileName(pkgFile); - var localFile = Path.Combine(localSourceDir, fileName); - var pkgContent = File.ReadAllText(pkgFile); + if (mode == SyncMode.Update) + { + Directory.CreateDirectory(localSourceDir); + } + + var expectedRelativePaths = sourcePackageFiles + .Select(f => f.RelativePath) + .ToHashSet(StringComparer.OrdinalIgnoreCase); - if (!File.Exists(localFile)) + var mismatches = new List(); + foreach (var sourceFile in sourcePackageFiles) { - // Create missing file from package content. - if (updateSnapshots) File.WriteAllText(localFile, pkgContent); - mismatches.Add($"Added missing file: {fileName}"); - continue; + var localFile = Path.Combine(localSourceDir, sourceFile.RelativePath.Replace('/', Path.DirectorySeparatorChar)); + var sourceContent = await GetGitHubStringAsync(httpClient, sourceFile.DownloadUrl).ConfigureAwait(false); + + if (!File.Exists(localFile)) + { + if (mode == SyncMode.Update) + { + var directory = Path.GetDirectoryName(localFile); + if (!string.IsNullOrEmpty(directory)) + Directory.CreateDirectory(directory); + File.WriteAllText(localFile, sourceContent); + } + + mismatches.Add($"Added missing file: {sourceFile.RelativePath}"); + continue; + } + + var localContent = File.ReadAllText(localFile); + if (!string.Equals(localContent.ReplaceLineEndings(), sourceContent.ReplaceLineEndings(), StringComparison.Ordinal)) + { + if (mode == SyncMode.Update) + File.WriteAllText(localFile, sourceContent); + + mismatches.Add($"Updated file: {sourceFile.RelativePath}"); + } } - var localContent = File.ReadAllText(localFile); - if (!string.Equals(localContent.ReplaceLineEndings(), pkgContent.ReplaceLineEndings(), StringComparison.Ordinal)) + if (Directory.Exists(localSourceDir)) { - // Regenerate local file to match package. - if (updateSnapshots) File.WriteAllText(localFile, pkgContent); - mismatches.Add($"Updated file: {fileName}"); + var localMirrorFiles = Directory.GetFiles(localSourceDir, "*", SearchOption.AllDirectories) + .Where(f => f.EndsWith(".xlf", StringComparison.OrdinalIgnoreCase) + || extensions.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) + .Select(f => Path.GetRelativePath(localSourceDir, f).Replace('\\', '/')) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var relativePath in expectedRelativePaths) + localMirrorFiles.Remove(relativePath); + + if (localMirrorFiles.Count > 0) + { + if (mode == SyncMode.Verify) + { + mismatches.Add("Extra local files (not in dotnet/sdk): " + string.Join(", ", localMirrorFiles.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))); + } + else + { + foreach (var remainingFile in localMirrorFiles.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + { + File.Delete(Path.Combine(localSourceDir, remainingFile)); + mismatches.Add($"Deleting extra local file (not in dotnet/sdk): {remainingFile}"); + } + } + } } + + if (mismatches.Count > 0) + { + var details = string.Join("\n", mismatches); + + if (mode == SyncMode.Verify) + { + throw new InvalidOperationException( + "Shared source for FileBasedPrograms is out of sync with dotnet/sdk. " + + "Run `dotnet run --file eng/ensure-sources-synced.cs -- --update` to refresh snapshots. Changes:\n" + + details); + } + + Console.WriteLine("Updated synced sources from dotnet/sdk:"); + Console.WriteLine(details); + return; + } + + Console.WriteLine("OK"); } -// If there are extra local files that are expected to mirror package files, report them but do not delete. -var localMirrorFiles = Directory.GetFiles(localSourceDir, "*", SearchOption.TopDirectoryOnly) - .Where(f => extensions.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) - .Select(Path.GetFileName) - .ToHashSet(StringComparer.OrdinalIgnoreCase); -foreach (var pkgName in packageFiles.Select(Path.GetFileName)) - localMirrorFiles.Remove(pkgName); -if (localMirrorFiles.Count > 0) +static SyncMode ParseMode(string[] args) { - mismatches.Add("Extra local files (not in package): " + string.Join(", ", localMirrorFiles)); + if (args.Length == 0) + { + var onCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")); + return onCi ? SyncMode.Verify : SyncMode.Update; + } + + if (args is ["--update"]) + return SyncMode.Update; + + if (args is ["--verify"]) + return SyncMode.Verify; + + throw new InvalidOperationException("Expected zero arguments (for default mode) or exactly one argument: --update or --verify."); } -if (mismatches.Count > 0) +static HttpClient CreateHttpClient() { - var action = updateSnapshots ? "Regenerated" : "Not regenerated in CI"; - throw new InvalidOperationException($"Shared source for FileBasedPrograms is out of sync with package. {action}. Changes:\n" + string.Join("\n", mismatches)); + var client = new HttpClient(); + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("roslyn-ensure-sources-synced", "1.0")); + return client; } -Console.WriteLine("OK"); +static async Task GetGitHubStringAsync(HttpClient client, string url) +{ + using var response = await client.GetAsync(url).ConfigureAwait(false); + var payload = await response.Content.ReadAsStringAsync().ConfigureAwait(false); -static string GetPackageVersion(string xmlFilePath, string propertyName) + if (IsRateLimitResponse(response.StatusCode, response.Headers, payload)) + { + throw new GitHubRateLimitException($"GitHub request hit rate limit at '{url}'."); + } + + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException($"GitHub request failed ({(int)response.StatusCode}) at '{url}'."); + } + + return payload; +} + +static async Task> GetDirectoryFilesAsync( + HttpClient client, + string commit, + string githubDirectoryPath, + Func includeFile, + Func mapRelativePath) +{ + var url = $"https://api.github.com/repos/dotnet/sdk/contents/{githubDirectoryPath}?ref={commit}"; + var payload = await GetGitHubStringAsync(client, url).ConfigureAwait(false); + + using var document = JsonDocument.Parse(payload); + if (document.RootElement.ValueKind != JsonValueKind.Array) + throw new InvalidOperationException($"Unexpected GitHub API response at '{url}'."); + + var files = new List(); + foreach (var item in document.RootElement.EnumerateArray()) + { + var type = item.GetProperty("type").GetString(); + if (!string.Equals(type, "file", StringComparison.Ordinal)) + continue; + + var name = item.GetProperty("name").GetString() + ?? throw new InvalidOperationException($"File entry missing 'name' in '{url}'."); + if (!includeFile(name)) + continue; + + var downloadUrl = item.GetProperty("download_url").GetString(); + if (string.IsNullOrWhiteSpace(downloadUrl)) + throw new InvalidOperationException($"File '{name}' has no download URL in '{url}'."); + + files.Add(new SyncedFile(mapRelativePath(name), downloadUrl)); + } + + files.Sort(static (a, b) => StringComparer.OrdinalIgnoreCase.Compare(a.RelativePath, b.RelativePath)); + return files; +} + +static bool IsRateLimitResponse(System.Net.HttpStatusCode statusCode, HttpResponseHeaders headers, string responseBody) +{ + if (statusCode == System.Net.HttpStatusCode.TooManyRequests) + return true; + + if (statusCode != System.Net.HttpStatusCode.Forbidden) + return false; + + if (headers.TryGetValues("X-RateLimit-Remaining", out var remainingValues) + && remainingValues.Any(static v => string.Equals(v, "0", StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + return responseBody.Contains("rate limit", StringComparison.OrdinalIgnoreCase); +} + +static void LogAzDoWarning(string message) { - var doc = XDocument.Load(xmlFilePath); - // Look for <{propertyName}>{version} - var packageVersionElement = doc.Descendants().FirstOrDefault(e => - string.Equals(e.Name.LocalName, propertyName, StringComparison.OrdinalIgnoreCase)) - ?? throw new InvalidOperationException($"'{propertyName}' not found in '{xmlFilePath}'"); - return packageVersionElement.Value; + Console.WriteLine($"##vso[task.logissue type=warning]{message}"); } -static string GetGlobalPackagesFolder() +enum SyncMode { - // Respect NUGET_PACKAGES if set; otherwise default location under user profile. - var envOverride = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); - if (!string.IsNullOrWhiteSpace(envOverride)) - return envOverride!; - - var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - if (string.IsNullOrWhiteSpace(userProfile)) - throw new InvalidOperationException("Cannot determine user profile path for global packages folder."); - return Path.Combine(userProfile, ".nuget", "packages"); + Update, + Verify, } + +readonly record struct SyncedFile(string RelativePath, string DownloadUrl); + +sealed class GitHubRateLimitException(string message) : Exception(message); diff --git a/eng/pipelines/insert.yml b/eng/pipelines/insert.yml index ac561c011921..9310876dabe6 100644 --- a/eng/pipelines/insert.yml +++ b/eng/pipelines/insert.yml @@ -18,15 +18,11 @@ parameters: type: string default: 'true' - - name: buildUserName + - name: devDivAzdoToken type: string - - name: buildPassword + - name: dncEngAzdoToken type: string - - name: componentUserName - type: string - - name: componentPassword - type: string - + - name: publishDataURI type: string - name: publishDataAccessToken @@ -52,15 +48,13 @@ parameters: - name: cherryPick type: string + default: '' steps: - checkout: none - - - task: NuGetCommand@2 - displayName: 'Install RIT from Azure Artifacts' - inputs: - command: custom - arguments: 'install RoslynTools.VisualStudioInsertionTool -PreRelease -Source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json' + + - script: dotnet tool install Microsoft.RoslynTools --tool-path .tools --prerelease --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json + displayName: 'Install roslyn-tools' - powershell: | $authorization = if ("" -ne $Env:PublishDataAccessToken) { "Bearer $Env:PublishDataAccessToken" } else { "" } @@ -76,7 +70,8 @@ steps: Write-Host "##vso[task.setvariable variable=Template.InsertToolset]$($true)" Write-Host "##vso[task.setvariable variable=Template.ComponentAzdoUri]$('')" Write-Host "##vso[task.setvariable variable=Template.ComponentProjectName]$('')" - Write-Host "##vso[task.setvariable variable=Template.DropPath]$('(default)')" + Write-Host "##vso[task.setvariable variable=Template.DropPath]$('')" + Write-Host "##vso[task.setvariable variable=Template.CherryPick]$('')" Write-Host "##vso[task.setvariable variable=Template.ComponentBranchName]$branchName" Write-Host "##vso[task.setvariable variable=Template.VSBranchName]$($insertionData.vsBranch)" @@ -107,15 +102,6 @@ steps: PublishDataAccessToken: ${{ parameters.publishDataAccessToken }} - powershell: | - # Set AzDO authorization template variables - Write-Host "Setting BuildUserName to $Env:BuildUserName" - Write-Host "##vso[task.setvariable variable=Template.BuildUserName]$Env:BuildUserName" - Write-Host "##vso[task.setvariable variable=Template.BuildPassword]$Env:BuildPassword" - - Write-Host "Setting ComponentUserName to $Env:ComponentUserName" - Write-Host "##vso[task.setvariable variable=Template.ComponentUserName]$Env:ComponentUserName" - Write-Host "##vso[task.setvariable variable=Template.ComponentPassword]$Env:ComponentPassword" - # Overwrite template variables with values passed into this template as parameters if ("" -ne $Env:CreateDraftPR) { @@ -161,12 +147,14 @@ steps: Write-Host "##vso[task.setvariable variable=Template.DropPath]$Env:DropPath" } + if ("" -ne $Env:CherryPick -and "(default)" -ne $Env:CherryPick) + { + Write-Host "Setting CherryPick to $Env:CherryPick" + Write-Host "##vso[task.setvariable variable=Template.CherryPick]$Env:CherryPick" + } + displayName: Set Variables from Input Parameters env: - BuildUserName: ${{ parameters.buildUserName }} - BuildPassword: ${{ parameters.buildPassword }} - ComponentUserName: ${{ parameters.componentUserName }} - ComponentPassword: ${{ parameters.componentPassword }} CreateDraftPR: ${{ parameters.createDraftPR }} AutoComplete: ${{ parameters.autoComplete }} TitlePrefix: ${{ parameters.titlePrefix }} @@ -174,43 +162,68 @@ steps: VSBranchName: ${{ parameters.vsBranchName }} ComponentBuildProjectName: ${{ parameters.componentBuildProjectName }} DropPath: ${{ parameters.dropPath }} + CherryPick: ${{ parameters.cherryPick }} # Now that everything is set, actually perform the insertion. - powershell: | - mv RoslynTools.VisualStudioInsertionTool.* RIT - .\RIT\tools\net472\OneOffInsertion.ps1 ` - -autoComplete "$(Template.AutoComplete)" ` - -buildQueueName "$(Build.DefinitionName)" ` - -cherryPick "${{ parameters.cherryPick }}" ` - -userName "$(Template.BuildUserName)" ` - -password "$(Template.BuildPassword)" ` - -componentUserName "$(Template.ComponentUserName)" ` - -componentPassword "$(Template.ComponentPassword)" ` - -componentAzdoUri "$(Template.ComponentAzdoUri)" ` - -componentProjectName "$(Template.ComponentProjectName)" ` - -componentName "Roslyn" ` - -componentGitHubRepoName "dotnet/roslyn" ` - -componentBranchName "$(Template.ComponentBranchName)" ` - -createDraftPR "$(Template.CreateDraftPR)" ` - -defaultValueSentinel "(default)" ` - -dropPath "$(Template.DropPath)" ` - -insertCore "(default)" ` - -insertDevDiv "(default)" ` - -insertionCount "1" ` - -insertToolset "$(Template.InsertToolset)" ` - -titlePrefix "$(Template.TitlePrefix)" ` - -titleSuffix "$(Template.TitleSuffix)" ` - -queueValidation "true" ` - -requiredValueSentinel "REQUIRED" ` - -reviewerGUID "6c25b447-1d90-4840-8fde-d8b22cb8733e" ` - -specificBuild "$(Build.BuildNumber)" ` - -updateAssemblyVersions "(default)" ` - -updateCoreXTLibraries "(default)" ` - -visualStudioBranchName "$(Template.VSBranchName)" ` - -writePullRequest "prid.txt" ` - -queueSpeedometerValidation "${{ parameters.queueSpeedometerValidation }}" ` - -retaininsertedbuild "${{ parameters.retainInsertedBuild }}" - displayName: 'Run OneOffInsertion.ps1' - - - script: 'echo. && echo. && type "prid.txt" && echo. && echo.' - displayName: 'Report PR URL' + $arguments = @( + "create-insertion" + "--insertion-name", "Roslyn" + "--vs-branch", "$(Template.VSBranchName)" + "--component-branch", "$(Template.ComponentBranchName)" + "--component-build-queue", "$(Build.DefinitionName)" + "--specific-build", "$(Build.BuildNumber)" + "--create-draft-pr", "$(Template.CreateDraftPR)" + "--set-auto-complete", "$(Template.AutoComplete)" + "--insert-toolset", "$(Template.InsertToolset)" + "--update-assembly-versions", "true" + "--run-speedometer-in-validation", "${{ parameters.queueSpeedometerValidation }}" + "--retain-inserted-build", "${{ parameters.retainInsertedBuild }}" + "--reviewer-guid", "6c25b447-1d90-4840-8fde-d8b22cb8733e" + "--devdiv-azdo-token", $Env:DevDivToken + "--dnceng-azdo-token", $Env:DncEngToken + "--ci" + ) + + $componentAzdoUri = "$(Template.ComponentAzdoUri)" + if ($componentAzdoUri -ne "") + { + $arguments += "--component-azdo-uri", $componentAzdoUri + } + + $componentProjectName = "$(Template.ComponentProjectName)" + if ($componentProjectName -ne "") + { + $arguments += "--component-project", $componentProjectName + } + + $dropPath = "$(Template.DropPath)" + if ($dropPath -ne "") + { + $arguments += "--build-drop-path", $dropPath + } + + $titlePrefix = "$(Template.TitlePrefix)" + if ($titlePrefix -ne "") + { + $arguments += "--title-prefix", $titlePrefix + } + + $titleSuffix = "$(Template.TitleSuffix)" + if ($titleSuffix -ne "") + { + $arguments += "--title-suffix", $titleSuffix + } + + $cherryPick = "$(Template.CherryPick)" + if ($cherryPick -ne "") + { + $arguments += "--cherry-pick", $cherryPick + } + + & ./.tools/roslyn-tools @arguments + displayName: 'Create VS Insertion PR' + env: + DevDivToken: ${{ parameters.devDivAzdoToken }} + DncEngToken: ${{ parameters.dncEngAzdoToken }} + DOTNET_ROLL_FORWARD: Major diff --git a/eng/pipelines/test-integration-helix.yml b/eng/pipelines/test-integration-helix.yml index 45e4bd5f1d0f..4c0415b1eb2e 100644 --- a/eng/pipelines/test-integration-helix.yml +++ b/eng/pipelines/test-integration-helix.yml @@ -3,6 +3,8 @@ parameters: type: string - name: queueName type: string + - name: buildQueueName + type: string - name: configuration type: string default: 'Debug' @@ -28,7 +30,7 @@ stages: configuration: ${{ parameters.configuration }} poolParameters: name: ${{ parameters.poolName }} - demands: ImageOverride -equals ${{ parameters.queueName }} + demands: ImageOverride -equals ${{ parameters.buildQueueName }} restoreArguments: -msbuildEngine vs buildArguments: -msbuildEngine vs /p:Projects='"$(Build.Repository.LocalPath)\src\VisualStudio\IntegrationTest\IntegrationTestBuildProject.csproj"' @@ -69,9 +71,9 @@ stages: # This is a temporary step until the actual test run moves to helix (then we would need to rehydrate the tests there instead) - task: BatchScript@1 - displayName: Rehydrate Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests (net10.0) + displayName: Rehydrate Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests (net10.0-windows) inputs: - filename: ./artifacts\bin\Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests/${{ parameters.configuration }}/net10.0/rehydrate.cmd + filename: ./artifacts\bin\Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests/${{ parameters.configuration }}/net10.0-windows/rehydrate.cmd env: HELIX_CORRELATION_PAYLOAD: '$(Build.SourcesDirectory)\.duplicate' diff --git a/eng/pipelines/test-unix-job-single-machine.yml b/eng/pipelines/test-unix-job-single-machine.yml index c7ded44eb00c..0cf3c25579a1 100644 --- a/eng/pipelines/test-unix-job-single-machine.yml +++ b/eng/pipelines/test-unix-job-single-machine.yml @@ -17,6 +17,9 @@ parameters: default: '' - name: poolParameters type: object +- name: locale + type: string + default: '' jobs: - job: ${{ parameters.jobName }} @@ -25,15 +28,21 @@ jobs: variables: DOTNET_ROLL_FORWARD: LatestMajor DOTNET_ROLL_FORWARD_TO_PRERELEASE: 1 + ${{ if ne(parameters.locale, '') }}: + LC_ALL: ${{ parameters.locale }} steps: - checkout: none + - script: sudo locale-gen ${{ parameters.locale }} + displayName: Generate Locale + condition: ne('${{ parameters.locale }}', '') + - task: DownloadPipelineArtifact@2 displayName: Download Test Payload inputs: artifact: ${{ parameters.testArtifactName }} path: '$(Build.SourcesDirectory)' - + - task: ShellScript@2 displayName: Rehydrate Unit Tests Environment inputs: diff --git a/eng/setup-pr-validation.ps1 b/eng/setup-pr-validation.ps1 index 37042156eb99..0b2bf1469099 100644 --- a/eng/setup-pr-validation.ps1 +++ b/eng/setup-pr-validation.ps1 @@ -21,32 +21,44 @@ try { git remote add gh https://github.com/dotnet/roslyn.git + Write-Host "Getting the hash of refs/pull/$prNumber/head..." + $remoteRef = git ls-remote gh refs/pull/$prNumber/head + Write-Host ($remoteRef | Out-String) + + $prHeadSHA = $remoteRef.Split()[0] + if ($enforceLatestCommit) { Write-Host "Validating the PR head matches the specified commit SHA ($commitSHA)..." - Write-Host "Getting the hash of refs/pull/$prNumber/head..." - $remoteRef = git ls-remote gh refs/pull/$prNumber/head - Write-Host ($remoteRef | Out-String) - - $prHeadSHA = $remoteRef.Split()[0] if (!$prHeadSHA.StartsWith($commitSHA)) { Write-Host "##vso[task.LogIssue type=error;]The PR's Head SHA ($prHeadSHA) does not begin with the specified commit SHA ($commitSHA). Unreviewed changes may have been pushed to the PR." exit 1 } } - Write-Host "Setting up the build for PR validation by pulling refs/pull/$prNumber/merge..." - git pull origin refs/pull/$prNumber/merge + Write-Host "Setting up the build for PR validation by fetching refs/pull/$prNumber/merge..." + git fetch gh refs/pull/$prNumber/merge if (!$?) { - Write-Host "##vso[task.LogIssue type=error;]Pulling branch refs/pull/$prNumber/merge failed." + Write-Host "##vso[task.LogIssue type=error;]Fetching ref refs/pull/$prNumber/merge failed." + exit 1 + } + + git checkout FETCH_HEAD + if (!$?) { + Write-Host "##vso[task.LogIssue type=error;]Checking out FETCH_HEAD for refs/pull/$prNumber/merge failed." exit 1 } if (!$enforceLatestCommit) { - Write-Host "Checking out the specified commit SHA ($commitSHA)..." - git checkout $commitSHA - if (!$?) { - Write-Host "##vso[task.LogIssue type=error;]Checking out commit SHA $commitSHA failed." - exit 1 + if ($prHeadSHA.StartsWith($commitSHA)) { + Write-Host "PR head SHA ($prHeadSHA) already matches the specified commit SHA ($commitSHA), skipping checkout." + } + else { + Write-Host "Checking out the specified commit SHA ($commitSHA)..." + git checkout $commitSHA + if (!$?) { + Write-Host "##vso[task.LogIssue type=error;]Checking out commit SHA $commitSHA failed." + exit 1 + } } } } diff --git a/eng/targets/GeneratePkgDef.targets b/eng/targets/GeneratePkgDef.targets index d14f08cf197b..5911e71f331b 100644 --- a/eng/targets/GeneratePkgDef.targets +++ b/eng/targets/GeneratePkgDef.targets @@ -71,8 +71,8 @@ (if we used Content items they would get included in the VSIX by VSSDK by default). --> - - + + diff --git a/eng/targets/Imports.targets b/eng/targets/Imports.targets index 1044a6efb109..a08f506ca3ba 100644 --- a/eng/targets/Imports.targets +++ b/eng/targets/Imports.targets @@ -48,9 +48,6 @@ true true - - - $(MSBuildWarningsAsMessages);NETSDK1206 - - - - diff --git a/eng/targets/Settings.props b/eng/targets/Settings.props index 868968830144..e40186752068 100644 --- a/eng/targets/Settings.props +++ b/eng/targets/Settings.props @@ -85,6 +85,11 @@ false + + + true + + net10.0 net8.0;net10.0 - net8.0 + net10.0-windows + net10.0 net10.0 - net8.0 + net10.0 net8.0 net10.0 @@ -33,6 +35,9 @@ $(NetCurrent) $(NetCurrent) $(NetCurrent) + $(NetCurrent) + $(NetCurrent) + $(NetCurrent) $(NetCurrent) $(NetCurrent) @@ -49,4 +54,5 @@ + diff --git a/eng/targets/TargetFrameworks.targets b/eng/targets/TargetFrameworks.targets new file mode 100644 index 000000000000..2dc65768cd16 --- /dev/null +++ b/eng/targets/TargetFrameworks.targets @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/eng/test-rebuild.ps1 b/eng/test-rebuild.ps1 index af71f45dbd68..2d1391b9df11 100644 --- a/eng/test-rebuild.ps1 +++ b/eng/test-rebuild.ps1 @@ -72,11 +72,11 @@ try { # Semantic Search reference assemblies can't be reconstructed from source. # The assemblies are not marked with ReferenceAssemblyAttribute attribute. - " --exclude net8.0\GeneratedRefAssemblies\Microsoft.CodeAnalysis.dll" + - " --exclude net8.0\GeneratedRefAssemblies\Microsoft.CodeAnalysis.CSharp.dll" + - " --exclude net8.0\GeneratedRefAssemblies\Microsoft.CodeAnalysis.VisualBasic.dll" + - " --exclude net8.0\GeneratedRefAssemblies\Microsoft.CodeAnalysis.SemanticSearch.Extensions.dll" + - " --exclude net8.0\GeneratedRefAssemblies\System.Collections.Immutable.dll" + + " --exclude net10.0\GeneratedRefAssemblies\Microsoft.CodeAnalysis.dll" + + " --exclude net10.0\GeneratedRefAssemblies\Microsoft.CodeAnalysis.CSharp.dll" + + " --exclude net10.0\GeneratedRefAssemblies\Microsoft.CodeAnalysis.VisualBasic.dll" + + " --exclude net10.0\GeneratedRefAssemblies\Microsoft.CodeAnalysis.SemanticSearch.Extensions.dll" + + " --exclude net10.0\GeneratedRefAssemblies\System.Collections.Immutable.dll" + " --debugPath `"$ArtifactsDir/BuildValidator`"" + " --sourcePath `"$RepoRoot/`"" + diff --git a/global.json b/global.json index 6ec5a7835b88..ac0231ec9126 100644 --- a/global.json +++ b/global.json @@ -1,11 +1,11 @@ { "sdk": { - "version": "10.0.100", + "version": "10.0.105", "allowPrerelease": false, "rollForward": "patch" }, "tools": { - "dotnet": "10.0.100", + "dotnet": "10.0.105", "vs": { "version": "17.14.0" }, @@ -13,8 +13,8 @@ "xcopy-msbuild": "18.0.0" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26055.1", - "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26055.1", + "Microsoft.DotNet.Arcade.Sdk": "10.0.0-beta.26201.4", + "Microsoft.DotNet.Helix.Sdk": "10.0.0-beta.26201.4", "Microsoft.Build.Traversal": "3.4.0" } } diff --git a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_ProgramMain.cs b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_ProgramMain.cs index 2f1bd4366343..e8336bbf4df1 100644 --- a/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_ProgramMain.cs +++ b/src/Analyzers/CSharp/Analyzers/ConvertProgram/ConvertProgramAnalysis_ProgramMain.cs @@ -56,7 +56,7 @@ private static bool CanOfferUseProgramMain(CodeStyleOption2 option, bool f public static Location GetUseProgramMainDiagnosticLocation(CompilationUnitSyntax root, bool isHidden) { // if the diagnostic is hidden, show it anywhere from the top of the file through the end of the last global - // statement. That way the user can make the change anywhere in teh top level code. Otherwise, just put + // statement. That way the user can make the change anywhere in the top level code. Otherwise, just put // the diagnostic on the start of the first global statement. if (!isHidden) return root.Members.OfType().First().GetFirstToken().GetLocation(); diff --git a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer.cs index 370eb44b0a60..7af5e667345a 100644 --- a/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/RemoveUnnecessaryImports/CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -28,6 +29,8 @@ public CSharpRemoveUnnecessaryImportsDiagnosticAnalyzer() { } + protected override GeneratedCodeAnalysisFlags GeneratedCodeAnalysisFlags => GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics; + protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; @@ -64,4 +67,16 @@ protected override IEnumerable GetFixableDiagnosticSpans( } } } + + protected override void AnalyzeSemanticModel(SemanticModelAnalysisContext context, SyntaxTree tree, CancellationToken cancellationToken) + { + // We've opted in to generated code analysis above, but we actually only want to analyze generated code for Razor + if (context.IsGeneratedCode && + tree.FilePath.IndexOf("Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator") == -1) + { + return; + } + + base.AnalyzeSemanticModel(context, tree, cancellationToken); + } } diff --git a/src/Analyzers/CSharp/Analyzers/SimplifyPropertyAccessor/CSharpSimplifyPropertyAccessorDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/SimplifyPropertyAccessor/CSharpSimplifyPropertyAccessorDiagnosticAnalyzer.cs index f104c65de68c..e26e748cb91e 100644 --- a/src/Analyzers/CSharp/Analyzers/SimplifyPropertyAccessor/CSharpSimplifyPropertyAccessorDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/SimplifyPropertyAccessor/CSharpSimplifyPropertyAccessorDiagnosticAnalyzer.cs @@ -24,7 +24,7 @@ public CSharpSimplifyPropertyAccessorDiagnosticAnalyzer() } public override DiagnosticAnalyzerCategory GetAnalyzerCategory() - => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis; + => DiagnosticAnalyzerCategory.SemanticSpanAnalysis; protected override void InitializeWorker(AnalysisContext context) => context.RegisterSyntaxNodeAction(AnalyzePropertyDeclaration, SyntaxKind.PropertyDeclaration); diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionInitializer/CSharpUseCollectionInitializerAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionInitializer/CSharpUseCollectionInitializerAnalyzer.cs index 65164118d935..9fd36ca74022 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionInitializer/CSharpUseCollectionInitializerAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionInitializer/CSharpUseCollectionInitializerAnalyzer.cs @@ -86,7 +86,7 @@ protected override bool AnalyzeMatchesAndCollectionConstructorForCollectionExpre return true; // See if we can specialize a single argument, by potentially spreading it, or dropping it entirely if redundant. - var supportsWithArgument = _objectCreationExpression.SyntaxTree.Options.LanguageVersion().IsCSharp14OrAbove(); + var supportsWithArgument = _objectCreationExpression.SyntaxTree.Options.LanguageVersion().IsCSharp15OrAbove(); if (TrySpecializeSingleArgument(out mayChangeSemantics)) return true; @@ -104,7 +104,7 @@ protected override bool AnalyzeMatchesAndCollectionConstructorForCollectionExpre return false; } - // Otherwise, if we're in C#14 or above, we can use the 'with(args)' argument trivially. + // Otherwise, if we're in C#15 or above, we can use the 'with(args)' argument trivially. if (supportsWithArgument) { preMatches.Add(new(argumentList, UseSpread: false, UseKeyValue: false)); diff --git a/src/Analyzers/CSharp/CodeFixes/MisplacedUsingDirectives/MisplacedUsingDirectivesCodeFixProvider.cs b/src/Analyzers/CSharp/CodeFixes/MisplacedUsingDirectives/MisplacedUsingDirectivesCodeFixProvider.cs index 56324b6508fe..91cb009c7bd7 100644 --- a/src/Analyzers/CSharp/CodeFixes/MisplacedUsingDirectives/MisplacedUsingDirectivesCodeFixProvider.cs +++ b/src/Analyzers/CSharp/CodeFixes/MisplacedUsingDirectives/MisplacedUsingDirectivesCodeFixProvider.cs @@ -109,7 +109,7 @@ private static ImmutableArray GetAllUsingDirectives(Compil foreach (var usingDirective in compilationUnit.Usings) { - // ignore global usings in teh compilation unit, they cannot be moved. + // ignore global usings in the compilation unit, they cannot be moved. if (usingDirective.GlobalKeyword == default) result.Add(usingDirective); } diff --git a/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpCollectionExpressionRewriter.cs b/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpCollectionExpressionRewriter.cs index 52c0eee938d9..bd914e617165 100644 --- a/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpCollectionExpressionRewriter.cs +++ b/src/Analyzers/CSharp/CodeFixes/UseCollectionExpression/CSharpCollectionExpressionRewriter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -484,17 +484,17 @@ IEnumerable CreateElements( { var arguments = invocation.ArgumentList.Arguments; yield return KeyValuePairElement( - IndentExpression(expressionStatement, arguments[0].Expression, preferredIndentation), + IndentNode(expressionStatement, arguments[0].Expression, preferredIndentation), ColonToken.WithTriviaFrom(arguments.GetSeparator(0)), - IndentExpression(expressionStatement, arguments[1].Expression, preferredIndentation)); + IndentNode(expressionStatement, arguments[1].Expression, preferredIndentation)); } else if (expressionStatement.Expression is AssignmentExpressionSyntax assignment) { var elementAccess = (ElementAccessExpressionSyntax)assignment.Left; yield return KeyValuePairElement( - IndentExpression(expressionStatement, elementAccess.ArgumentList.Arguments[0].Expression, preferredIndentation), + IndentNode(expressionStatement, elementAccess.ArgumentList.Arguments[0].Expression, preferredIndentation), ColonToken.WithTrailingTrivia(assignment.OperatorToken.TrailingTrivia), - IndentExpression(expressionStatement, assignment.Right, preferredIndentation)); + IndentNode(expressionStatement, assignment.Right, preferredIndentation)); } else { @@ -506,7 +506,7 @@ IEnumerable CreateElements( } else { - var expressions = ConvertExpressions(expressionStatement.Expression, expr => IndentExpression(expressionStatement, expr, preferredIndentation)); + var expressions = ConvertExpressions(expressionStatement.Expression, expr => IndentNode(expressionStatement, expr, preferredIndentation)); Contract.ThrowIfTrue(expressions.Length >= 2 && match.UseSpread); @@ -535,7 +535,7 @@ IEnumerable CreateElements( } else if (node is ForEachStatementSyntax foreachStatement) { - var indentedExpression = IndentExpression(foreachStatement, foreachStatement.Expression, preferredIndentation); + var indentedExpression = IndentNode(foreachStatement, foreachStatement.Expression, preferredIndentation); if (match.UseCast) { @@ -562,7 +562,7 @@ IEnumerable CreateElements( } else if (node is IfStatementSyntax ifStatement) { - var condition = IndentExpression(ifStatement, ifStatement.Condition, preferredIndentation).Parenthesize(includeElasticTrivia: false); + var condition = IndentNode(ifStatement, ifStatement.Condition, preferredIndentation).Parenthesize(includeElasticTrivia: false); var trueStatement = (ExpressionStatementSyntax)UnwrapEmbeddedStatement(ifStatement.Statement); if (ifStatement.Else is null) @@ -588,11 +588,13 @@ IEnumerable CreateElements( } else if (node is ExpressionSyntax expression) { - yield return CreateCollectionElement(match.UseSpread, IndentExpression(parentStatement: null, expression, preferredIndentation)); + yield return CreateCollectionElement(match.UseSpread, IndentNode(parentStatement: null, expression, preferredIndentation)); } else if (node is ArgumentListSyntax argumentList) { - yield return WithElement(argumentList.WithoutTrivia()); + var indentedArgumentList = IndentNode(parentStatement: null, argumentList, preferredIndentation); + yield return WithElement(indentedArgumentList.WithoutTrivia()) + .WithLeadingTrivia(indentedArgumentList.GetLeadingTrivia()); } else { @@ -600,34 +602,34 @@ IEnumerable CreateElements( } } - ExpressionSyntax IndentExpression( + TNode IndentNode( StatementSyntax? parentStatement, - ExpressionSyntax expression, - string? preferredIndentation) + TNode node, + string? preferredIndentation) where TNode : SyntaxNode { // This must be called from an expression from the original tree. Not something we're already transforming. // Otherwise, we'll have no idea how to apply the preferredIndentation if present. - Contract.ThrowIfNull(expression.Parent); + Contract.ThrowIfNull(node.Parent); if (preferredIndentation is null) - return expression.WithoutLeadingTrivia(); + return node.WithoutLeadingTrivia(); - var startLine = document.Text.Lines.GetLineFromPosition(GetAnchorNode(expression).SpanStart); + var startLine = document.Text.Lines.GetLineFromPosition(GetAnchorNode(node).SpanStart); var firstTokenOnLineIndentationString = GetIndentationStringForToken(document.Root.FindToken(startLine.Start)); - var expressionFirstToken = expression.GetFirstToken(); - var updatedExpression = expression.ReplaceTokens( - expression.DescendantTokens(), + var nodeFirstToken = node.GetFirstToken(); + var updatedNode = node.ReplaceTokens( + node.DescendantTokens(), (currentToken, _) => { // Ensure the first token has the indentation we're moving the entire node to - if (currentToken == expressionFirstToken) + if (currentToken == nodeFirstToken) return currentToken.WithLeadingTrivia(Whitespace(preferredIndentation)); return IndentToken(currentToken, preferredIndentation, firstTokenOnLineIndentationString); }); - // Now, once we've indented the expression, attempt to move comments on its containing statement to it. - return TransferParentStatementComments(parentStatement, updatedExpression, preferredIndentation); + // Now, once we've indented the node, attempt to move comments on its containing statement to it. + return TransferParentStatementComments(parentStatement, updatedNode, preferredIndentation); SyntaxNode GetAnchorNode(SyntaxNode node) { @@ -711,13 +713,13 @@ SyntaxTrivia GetIndentedWhitespaceTrivia(string preferredIndentation, string fir : preferredIndentation); } - static ExpressionSyntax TransferParentStatementComments( + static TNode TransferParentStatementComments( StatementSyntax? parentStatement, - ExpressionSyntax expression, - string preferredIndentation) + TNode node, + string preferredIndentation) where TNode : SyntaxNode { if (parentStatement is null) - return expression; + return node; using var _1 = ArrayBuilder.GetInstance(out var newLeadingTrivia); using var _2 = ArrayBuilder.GetInstance(out var newTrailingTrivia); @@ -768,11 +770,11 @@ static ExpressionSyntax TransferParentStatementComments( } } - expression = expression + node = node .WithPrependedLeadingTrivia(newLeadingTrivia) .WithAppendedTrailingTrivia(newTrailingTrivia); - return expression; + return node; } string GetIndentationStringForToken(SyntaxToken token) diff --git a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems index 368a083c4c6c..ed1d91b95164 100644 --- a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems +++ b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems @@ -39,6 +39,7 @@ + @@ -212,4 +213,4 @@ - \ No newline at end of file + diff --git a/src/Analyzers/CSharp/Tests/GenerateMethod/GenerateMethodTests.cs b/src/Analyzers/CSharp/Tests/GenerateMethod/GenerateMethodTests.cs index d07d256ae59b..0b4e31630616 100644 --- a/src/Analyzers/CSharp/Tests/GenerateMethod/GenerateMethodTests.cs +++ b/src/Analyzers/CSharp/Tests/GenerateMethod/GenerateMethodTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics.GenerateMethod; [Trait(Traits.Feature, Traits.Features.CodeActionsGenerateMethod)] -public sealed class GenerateMethodTests(ITestOutputHelper logger) : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor(logger) +public sealed partial class GenerateMethodTests(ITestOutputHelper logger) : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest_NoEditor(logger) { internal override (DiagnosticAnalyzer?, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace) => (null, new GenerateMethodCodeFixProvider()); diff --git a/src/Analyzers/CSharp/Tests/GenerateMethod/GenerateMethodTests_Razor.cs b/src/Analyzers/CSharp/Tests/GenerateMethod/GenerateMethodTests_Razor.cs new file mode 100644 index 000000000000..296dab636720 --- /dev/null +++ b/src/Analyzers/CSharp/Tests/GenerateMethod/GenerateMethodTests_Razor.cs @@ -0,0 +1,275 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.CodeFixes.GenerateMethod; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics.GenerateMethod; + +public sealed partial class GenerateMethodTests +{ + private sealed record RazorGeneratedDocumentData(string HintName, string GeneratedCode); + + protected override void InitializeWorkspace(TestWorkspace workspace, TestParameters parameters) + { + if (parameters.fixProviderData is not RazorGeneratedDocumentData generatedDocument) + { + return; + } + + var project = workspace.CurrentSolution.Projects.Single(); + var updatedProject = project.AddAnalyzerReference( + new TestGeneratorReference( + new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator( + context => context.AddSource(generatedDocument.HintName, generatedDocument.GeneratedCode)))); + + Assert.True(workspace.TryApplyChanges(updatedProject.Solution)); + } + + private static TestParameters WithRazorGeneratedDocument(string generatedCode, string hintName = "Component.razor.g.cs") + => new(fixProviderData: new RazorGeneratedDocumentData(hintName, generatedCode)); + + private async Task TestGeneratingInRazorSourceGeneratedDocumentAsync( + string workspaceMarkup, + string generatedCode, + string expectedGeneratedCode, + bool requestFromSourceGeneratedDocument) + { + using var workspace = CreateWorkspaceFromOptions(workspaceMarkup, WithRazorGeneratedDocument(generatedCode)); + + var project = workspace.CurrentSolution.Projects.Single(); + var sourceGeneratedDocument = Assert.Single(await project.GetSourceGeneratedDocumentsAsync(CancellationToken.None)); + Assert.Contains("Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator", sourceGeneratedDocument.FilePath); + Assert.StartsWith( + "// ", + (await sourceGeneratedDocument.GetTextAsync(CancellationToken.None)).ToString()); + + var requestDocument = requestFromSourceGeneratedDocument + ? sourceGeneratedDocument + : project.Documents.Single(); + + var requestTree = await requestDocument.GetSyntaxTreeAsync(CancellationToken.None); + var compilation = await project.GetCompilationAsync(CancellationToken.None); + Assert.NotNull(requestTree); + Assert.NotNull(compilation); + + var diagnostic = Assert.Single( + compilation!.GetDiagnostics(CancellationToken.None).Where( + diagnostic => diagnostic.Severity == DiagnosticSeverity.Error && diagnostic.Location.SourceTree == requestTree)); + + var codeActions = new List(); + var context = new CodeFixContext( + requestDocument, + diagnostic.Location.SourceSpan, + [diagnostic], + (action, _) => codeActions.Add(action), + CancellationToken.None); + + var provider = new GenerateMethodCodeFixProvider(); + await provider.RegisterCodeFixesAsync(context); + + var operations = await Assert.Single(codeActions).GetOperationsAsync(CancellationToken.None); + var changedSolutions = await ApplyOperationsAndGetSolutionAsync(workspace, operations); + var newSourceGeneratedDocument = await changedSolutions.Item2.GetSourceGeneratedDocumentAsync(sourceGeneratedDocument.Id, CancellationToken.None); + + Assert.NotNull(newSourceGeneratedDocument); + AssertEx.EqualOrDiff( + expectedGeneratedCode, + (await newSourceGeneratedDocument!.GetTextAsync(CancellationToken.None)).ToString()); + } + + [Fact] + public Task TestGeneratingIntoRazorSourceGeneratedDocumentFromRegularDocument() + => TestGeneratingInRazorSourceGeneratedDocumentAsync( + """ + + + + public class C + { + public void M() + { + Component.Me$$thod(); + } + } + + + + """, + """ + // + + using System; + + public partial class Component + { + } + """, + """ + // + + using System; + + public partial class Component + { + internal static void Method() + { + throw new NotImplementedException(); + } + } + """, + requestFromSourceGeneratedDocument: false); + + [Fact] + public Task TestGeneratingInRazorSourceGeneratedDocument() + => TestGeneratingInRazorSourceGeneratedDocumentAsync( + """ + + + + public class Placeholder + { + } + + + + """, + """ + // + + using System; + + public partial class Component + { + public void M() + { + MissingMethod(); + } + } + """, + """ + // + + using System; + + public partial class Component + { + public void M() + { + MissingMethod(); + } + + private void MissingMethod() + { + throw new NotImplementedException(); + } + } + """, + requestFromSourceGeneratedDocument: true); + + [Fact] + public Task TestGeneratingIntoHiddenRazorSourceGeneratedDocumentFromRegularDocument() + => TestGeneratingInRazorSourceGeneratedDocumentAsync( + """ + + + + public class C + { + public void M() + { + Component.Me$$thod(); + } + } + + + + """, + """ + // + + using System; + + #line hidden + public partial class Component + { + } + #line default + """, + """ + // + + using System; + + #line hidden + public partial class Component + { + internal static void Method() + { + throw new NotImplementedException(); + } + } + #line default + """, + requestFromSourceGeneratedDocument: false); + + [Fact] + public Task TestGeneratingInHiddenRazorSourceGeneratedDocument() + => TestGeneratingInRazorSourceGeneratedDocumentAsync( + """ + + + + public class Placeholder + { + } + + + + """, + """ + // + + using System; + + #line hidden + public partial class Component + { + public void M() + { + MissingMethod(); + } + } + #line default + """, + """ + // + + using System; + + #line hidden + public partial class Component + { + public void M() + { + MissingMethod(); + } + + private void MissingMethod() + { + throw new NotImplementedException(); + } + } + #line default + """, + requestFromSourceGeneratedDocument: true); +} diff --git a/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.cs b/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.cs index fd36f883acd5..b98c9362ba35 100644 --- a/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.cs +++ b/src/Analyzers/CSharp/Tests/ImplementInterface/ImplementInterfaceCodeFixTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.CodeStyle; @@ -11888,4 +11889,54 @@ class C : IDisposable LanguageVersion = LanguageVersion.CSharp14, Options = { { FormattingOptions2.NewLine, "\n" } }, }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82787")] + public Task FileLevelDirective_AddUsings() + => new VerifyCS.Test + { + TestCode = """ + #:property Configuration=Release + + using System.Collections; + + class A : {|CS0535:I|} + { + } + + interface I + { + void M(System.DateTime dt, System.IO.FileInfo f); + } + """, + FixedCode = """ + #:property Configuration=Release + + using System; + using System.Collections; + using System.IO; + + class A : I + { + public void M(DateTime dt, FileInfo f) + { + throw new NotImplementedException(); + } + } + + interface I + { + void M(System.DateTime dt, System.IO.FileInfo f); + } + """, + SolutionTransforms = + { + static (solution, projectId) => + { + var project = solution.GetProject(projectId)!; + var parseOptions = (CSharpParseOptions)project.ParseOptions!; + return solution.WithProjectParseOptions(projectId, + parseOptions.WithFeatures(parseOptions.Features.Append(new("FileBasedProgram", "")))); + }, + }, + }.RunAsync(); } diff --git a/src/Analyzers/CSharp/Tests/RemoveUnnecessaryImports/RemoveUnnecessaryImportsTests.cs b/src/Analyzers/CSharp/Tests/RemoveUnnecessaryImports/RemoveUnnecessaryImportsTests.cs index 34aafe8a672d..80db8a2c8665 100644 --- a/src/Analyzers/CSharp/Tests/RemoveUnnecessaryImports/RemoveUnnecessaryImportsTests.cs +++ b/src/Analyzers/CSharp/Tests/RemoveUnnecessaryImports/RemoveUnnecessaryImportsTests.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.RemoveUnnecessaryImports; using Microsoft.CodeAnalysis.Editor.UnitTests.CodeActions; +using Microsoft.CodeAnalysis.RemoveUnnecessaryImports; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Testing; @@ -193,6 +194,54 @@ static void Main(string[] args) """, }.RunAsync(); + [Fact] + public Task TestRazorGeneratedCode() + => new VerifyCS.Test + { + TestCode = """ + // + + using System; + using System.Collections.Generic; + using System.Linq; + + class Program + { + static void Main(string[] args) + { + List d; + } + } + """, + SolutionTransforms = + { + (solution, projectId) => + { + var razorGenerator = new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator((c) => c.AddSource("Component.razor.g.cs", """ + using System; + using System.Collections.Generic; + + public class C + { + public void M(List items) + { + } + } + """)); + + var project = solution.GetProject(projectId)!; + var updatedProject = project.AddAnalyzerReference(new TestGeneratorReference(razorGenerator)); + return updatedProject.Solution; + } + }, + ExpectedDiagnostics = { + // Microsoft.CodeAnalysis.Test.Utilities\Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator\Component.razor.g.cs(1,1): hidden IDE0005_gen: Using directive is unnecessary. + VerifyCS.Diagnostic(RemoveUnnecessaryImportsConstants.IDE0005_gen).WithSpan(@"Microsoft.CodeAnalysis.Test.Utilities\Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator\Component.razor.g.cs", 1, 1, 1, 14), + // Microsoft.CodeAnalysis.Test.Utilities\Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator\Component.razor.g.cs(1,1): hidden RemoveUnnecessaryImportsFixable: + VerifyCS.Diagnostic(RemoveUnnecessaryImportsConstants.DiagnosticFixableId).WithSpan(@"Microsoft.CodeAnalysis.Test.Utilities\Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator\Component.razor.g.cs", 1, 1, 2, 34), + } + }.RunAsync(); + [Fact] public Task TestGenericReferenceInTypeContext() => VerifyCS.VerifyCodeFixAsync( diff --git a/src/Analyzers/CSharp/Tests/RemoveUnusedParametersAndValues/RemoveUnusedValueAssignmentTests.cs b/src/Analyzers/CSharp/Tests/RemoveUnusedParametersAndValues/RemoveUnusedValueAssignmentTests.cs index af603723f36c..dda46b6f79dd 100644 --- a/src/Analyzers/CSharp/Tests/RemoveUnusedParametersAndValues/RemoveUnusedValueAssignmentTests.cs +++ b/src/Analyzers/CSharp/Tests/RemoveUnusedParametersAndValues/RemoveUnusedValueAssignmentTests.cs @@ -8386,6 +8386,76 @@ static void T1(ref int param, bool flag) } """); + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/44100")] + public Task RefParameter_WrittenBeforeMethodThatThrows() + => new VerifyCS.Test + { + TestCode = """ + using System; + + class Program + { + static void Write(ref int value) + { + value = 1; + Throw(); + value = 2; + } + + static void Throw() => throw new Exception(); + } + """, + Options = + { + { CSharpCodeStyleOptions.UnusedValueAssignment, UnusedValuePreference.DiscardVariable, NotificationOption2.Suggestion }, + }, + }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/44100")] + public Task OutParameter_WrittenMultipleTimes() + => new VerifyCS.Test + { + TestCode = """ + using System; + + class C + { + static void M(out int value) + { + value = 1; + value = 2; + } + } + """, + Options = + { + { CSharpCodeStyleOptions.UnusedValueAssignment, UnusedValuePreference.DiscardVariable, NotificationOption2.Suggestion }, + }, + }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/44100")] + public Task RefLocal_WrittenMultipleTimes() + => new VerifyCS.Test + { + TestCode = """ + class C + { + int _field; + + void M() + { + ref int r = ref _field; + r = 1; + r = 2; + } + } + """, + Options = + { + { CSharpCodeStyleOptions.UnusedValueAssignment, UnusedValuePreference.DiscardVariable, NotificationOption2.Suggestion }, + }, + }.RunAsync(); + [Fact] public Task LocalFunction_OutParameter_UsedInCaller() => TestDiagnosticMissingAsync( diff --git a/src/Analyzers/CSharp/Tests/SimplifyLinqExpression/CSharpSimplifyLinqExpressionTests.cs b/src/Analyzers/CSharp/Tests/SimplifyLinqExpression/CSharpSimplifyLinqExpressionTests.cs index 2bd82856f50c..ec1bff350099 100644 --- a/src/Analyzers/CSharp/Tests/SimplifyLinqExpression/CSharpSimplifyLinqExpressionTests.cs +++ b/src/Analyzers/CSharp/Tests/SimplifyLinqExpression/CSharpSimplifyLinqExpressionTests.cs @@ -634,4 +634,100 @@ public void Test(int[] numbers) } """ }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82388")] + public Task FixParenthesizedExpression() + => new VerifyCS.Test + { + TestCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + static void M(string[] args) + { + var v = (decimal)[|(args.Select(x => int.Parse(x))).Sum()|]; + } + } + """, + FixedCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + static void M(string[] args) + { + var v = (decimal)args.Sum(x => int.Parse(x)); + } + } + """, + }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82388")] + public Task FixNullableSuppressionExpression() + => new VerifyCS.Test + { + TestCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + static void M(string[] args) + { + var v = (decimal)[|args.Select(x => int.Parse(x))!.Sum()|]; + } + } + """, + FixedCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + static void M(string[] args) + { + var v = (decimal)args.Sum(x => int.Parse(x)); + } + } + """, + }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82388")] + public Task FixParenthesizedAndNullableSuppressionExpression() + => new VerifyCS.Test + { + TestCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + static void M(string[] args) + { + var v = (decimal)[|(({|CS8715:args.Select(x => int.Parse(x))|})!)!.Sum()|]; + } + } + """, + FixedCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + static void M(string[] args) + { + var v = (decimal)args.Sum(x => int.Parse(x)); + } + } + """, + }.RunAsync(); } diff --git a/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests_CollectionExpression.cs b/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests_CollectionExpression.cs index a5fca188ef11..58175cd749ec 100644 --- a/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests_CollectionExpression.cs +++ b/src/Analyzers/CSharp/Tests/UseCollectionInitializer/UseCollectionInitializerTests_CollectionExpression.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -5865,7 +5865,7 @@ void M() """); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestObjectCreationArgument1_CSharp14() + public Task TestObjectCreationArgument1_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -5897,7 +5897,7 @@ void M(int[] values) }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestObjectCreationArgument2_CSharp14() + public Task TestObjectCreationArgument2_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -5929,7 +5929,7 @@ void M(int[] values) }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestObjectCreationArgument3_CSharp14() + public Task TestObjectCreationArgument3_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -5961,7 +5961,7 @@ void M(int[] values) }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestObjectCreationArgument4_CSharp14() + public Task TestObjectCreationArgument4_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -5992,6 +5992,60 @@ void M(int[] values) ReferenceAssemblies = ReferenceAssemblies.Net.Net80, }.RunAsync(); + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/74208")] + public Task TestObjectCreationArgument5_CSharp14() + => new VerifyCS.Test + { + TestCode = $$""" + using System.Collections.Generic; + using System; + + class C + { + void M() + { + Dictionary goo = new(StringComparer.OrdinalIgnoreCase); + } + } + """, + LanguageVersion = LanguageVersion.CSharp14, + ReferenceAssemblies = ReferenceAssemblies.Net.Net100, + }.RunAsync(); + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/74208")] + public Task TestObjectCreationArgument5_CSharp15() + => new VerifyCS.Test + { + TestCode = $$""" + using System.Collections.Generic; + using System; + + class C + { + void M() + { + Dictionary goo = [|new|](StringComparer.OrdinalIgnoreCase); + } + } + """, + FixedCode = """ + using System.Collections.Generic; + using System; + + class C + { + void M() + { + Dictionary goo = [with(StringComparer.OrdinalIgnoreCase)]; + } + } + """, + LanguageVersion = LanguageVersionExtensions.CSharpNext, + ReferenceAssemblies = ReferenceAssemblies.Net.Net100, + }.RunAsync(); + [Fact] public Task TestNonGenericCollectionBuilder() => TestInRegularAndScriptAsync( @@ -6061,10 +6115,286 @@ void M() } """); + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/83029")] + public Task TestWithOverMultipleLines1() + => new VerifyCS.Test + { + TestCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = [|new|](StringComparer.Ordinal) + { + "a", + "b" + }; + } + } + """, + FixedCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = + [ + with(StringComparer.Ordinal), + "a", + "b" + ]; + } + } + """, + LanguageVersion = LanguageVersionExtensions.CSharpNext, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/83029")] + public Task TestWithOverMultipleLines2() + => new VerifyCS.Test + { + TestCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = [|new|](StringComparer.Ordinal) + { + "a" + + "_suffix", + "b" + }; + } + } + """, + FixedCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = + [ + with(StringComparer.Ordinal), + "a" + + "_suffix", + "b" + ]; + } + } + """, + LanguageVersion = LanguageVersionExtensions.CSharpNext, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/83029")] + public Task TestWithOverMultipleLines3() + => new VerifyCS.Test + { + TestCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = [|new|]( + StringComparer.Ordinal) + { + "a", + "b" + }; + } + } + """, + FixedCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = + [ + with( + StringComparer.Ordinal), + "a", + "b" + ]; + } + } + """, + LanguageVersion = LanguageVersionExtensions.CSharpNext, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/83029")] + public Task TestWithOverMultipleLines4() + => new VerifyCS.Test + { + TestCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = [|new|]( + StringComparer.Ordinal) + { + "a" + + "_suffix", + "b" + }; + } + } + """, + FixedCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = + [ + with( + StringComparer.Ordinal), + "a" + + "_suffix", + "b" + ]; + } + } + """, + LanguageVersion = LanguageVersionExtensions.CSharpNext, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/83029")] + public Task TestWithOverMultipleLines5() + => new VerifyCS.Test + { + TestCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = [|new|]( + true ? StringComparer.Ordinal + : StringComparer.OrdinalIgnoreCase) + { + "a", + "b" + }; + } + } + """, + FixedCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = + [ + with( + true ? StringComparer.Ordinal + : StringComparer.OrdinalIgnoreCase), + "a", + "b" + ]; + } + } + """, + LanguageVersion = LanguageVersionExtensions.CSharpNext, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }.RunAsync(); + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/83029")] + public Task TestWithOverMultipleLines6() + => new VerifyCS.Test + { + TestCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = [|new|]( + true ? StringComparer.Ordinal + : StringComparer.OrdinalIgnoreCase) + { + "a" + + "_suffix", + "b" + }; + } + } + """, + FixedCode = """ + using System; + using System.Linq; + using System.Collections.Generic; + + class C + { + void M(int[] values) + { + HashSet h = + [ + with( + true ? StringComparer.Ordinal + : StringComparer.OrdinalIgnoreCase), + "a" + + "_suffix", + "b" + ]; + } + } + """, + LanguageVersion = LanguageVersionExtensions.CSharpNext, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + }.RunAsync(); + // Enable when dictionary-expressions come online. #if false [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestKeyValuePair1_CSharp14() + public Task TestKeyValuePair1_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -6096,7 +6426,7 @@ void M(int[] values) }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestKeyValuePair2_CSharp14() + public Task TestKeyValuePair2_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -6136,7 +6466,7 @@ void M(int[] values) }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestKeyValuePair3_CSharp14() + public Task TestKeyValuePair3_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -6168,7 +6498,7 @@ void M(int[] values) }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestKeyValuePair4_CSharp14() + public Task TestKeyValuePair4_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -6208,7 +6538,7 @@ void M(int[] values) }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestKeyValuePair5_CSharp14() + public Task TestKeyValuePair5_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -6241,7 +6571,7 @@ void M(int[] values) }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestKeyValuePair6_CSharp14() + public Task TestKeyValuePair6_CSharp15() => new VerifyCS.Test { TestCode = """ @@ -6274,7 +6604,7 @@ void M(int[] values) }.RunAsync(); [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72699")] - public Task TestKeyValuePair7_CSharp14() + public Task TestKeyValuePair7_CSharp15() => new VerifyCS.Test { TestCode = """ diff --git a/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AbstractAddRequiredParenthesesDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AbstractAddRequiredParenthesesDiagnosticAnalyzer.cs index e7ce5526c2da..8493242af5b1 100644 --- a/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AbstractAddRequiredParenthesesDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/AddRequiredParentheses/AbstractAddRequiredParenthesesDiagnosticAnalyzer.cs @@ -133,7 +133,7 @@ bool IsClearPrecedenceBoundary() // case if grouping didn't operate as expected. // // this is not always the case though. `??` in particular can be quite confusing as it generally - // operates in teh same type domain (or the nullable extension of that type). For example: + // operates in the same type domain (or the nullable extension of that type). For example: // // a + b ?? c // diff --git a/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs b/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs index 0b8f4a764f77..b48ff33570d1 100644 --- a/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs +++ b/src/Analyzers/Core/Analyzers/Helpers/DiagnosticHelper.cs @@ -329,7 +329,7 @@ static IEnumerable GetEffectiveCustomTags(DiagnosticDescriptor descripto // These diagnostics are hidden and not configurable, so help link can never be shown and is not applicable. if (id == RemoveUnnecessaryImports.RemoveUnnecessaryImportsConstants.DiagnosticFixableId || - id == "IDE0005_gen") + id == RemoveUnnecessaryImports.RemoveUnnecessaryImportsConstants.IDE0005_gen) { return null; } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs index 466d9d23ddde..bbcdc8ebb3ef 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs @@ -87,6 +87,11 @@ private void AnalyzeSemanticModel(SemanticModelAnalysisContext context) var tree = context.SemanticModel.SyntaxTree; var cancellationToken = context.CancellationToken; + AnalyzeSemanticModel(context, tree, cancellationToken); + } + + protected virtual void AnalyzeSemanticModel(SemanticModelAnalysisContext context, SyntaxTree tree, CancellationToken cancellationToken) + { var unnecessaryImports = UnnecessaryImportsProvider.GetUnnecessaryImports(context.SemanticModel, context.FilterSpan, cancellationToken); if (unnecessaryImports.Any()) { @@ -97,7 +102,7 @@ private void AnalyzeSemanticModel(SemanticModelAnalysisContext context) // for us appropriately. var mergedImports = MergeImports(unnecessaryImports); - var descriptor = GeneratedCodeUtilities.IsGeneratedCode(tree, IsRegularCommentOrDocComment, cancellationToken) + var descriptor = context.IsGeneratedCode ? _generatedCodeClassificationIdDescriptor : _classificationIdDescriptor; var contiguousSpans = GetContiguousSpans(mergedImports); diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/RemoveUnnecessaryImportsConstants.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/RemoveUnnecessaryImportsConstants.cs index 4e9b025ce46d..28332abe7c37 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/RemoveUnnecessaryImportsConstants.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/RemoveUnnecessaryImportsConstants.cs @@ -8,4 +8,6 @@ internal static class RemoveUnnecessaryImportsConstants { // NOTE: This is a trigger diagnostic, which doesn't show up in the ruleset editor and hence doesn't need a conventional IDE Diagnostic ID string. public const string DiagnosticFixableId = "RemoveUnnecessaryImportsFixable"; + + public const string IDE0005_gen = "IDE0005_gen"; } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs index 1b75719ebc71..323e2baa5d86 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnusedParametersAndValues/AbstractRemoveUnusedParametersAndValuesDiagnosticAnalyzer.SymbolStartAnalyzer.BlockAnalyzer.cs @@ -591,8 +591,6 @@ bool ShouldReportUnusedValueDiagnostic( SymbolUsageResult resultFromFlowAnalysis, out ImmutableDictionary? properties) { - Debug.Assert(symbol is not ILocalSymbol local || !local.IsRef); - properties = null; // Bail out in following cases: @@ -601,7 +599,13 @@ bool ShouldReportUnusedValueDiagnostic( // 3. Static local symbols. Assignment to static locals // is not unnecessary as the assigned value can be used on the next invocation. // 4. Ignore special discard symbol names (see https://github.com/dotnet/roslyn/issues/32923). - if (_options.UnusedValueAssignmentSeverity.Severity == ReportDiagnostic.Suppress || + // 5. By-ref parameter or local symbols. Writes to by-ref symbols are + // visible to the caller and may be observed across threads, so they + // should not be flagged as redundant + // (see https://github.com/dotnet/roslyn/issues/44100). + if (symbol is IParameterSymbol { RefKind: not RefKind.None } || + symbol is ILocalSymbol { RefKind: not RefKind.None } || + _options.UnusedValueAssignmentSeverity.Severity == ReportDiagnostic.Suppress || symbol.GetSymbolType().IsErrorType() || (symbol.IsStatic && symbol.Kind == SymbolKind.Local) || symbol.IsSymbolWithSpecialDiscardName()) diff --git a/src/Analyzers/Core/CodeFixes/AliasAmbiguousType/AbstractAliasAmbiguousTypeCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/AliasAmbiguousType/AbstractAliasAmbiguousTypeCodeFixProvider.cs index 6b465f0cd097..5ae0eff83440 100644 --- a/src/Analyzers/Core/CodeFixes/AliasAmbiguousType/AbstractAliasAmbiguousTypeCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/AliasAmbiguousType/AbstractAliasAmbiguousTypeCodeFixProvider.cs @@ -117,7 +117,7 @@ ImmutableArray GetNameSegments(ITypeSymbol symbol) result.Add(current.Name); } - // We walked upwards to get the name segments. So reverse teh order here so it goes from outer-most to + // We walked upwards to get the name segments. So reverse the order here so it goes from outer-most to // inner-most names. result.ReverseContents(); return result.ToImmutableAndClear(); diff --git a/src/Analyzers/Core/CodeFixes/GenerateMember/AbstractGenerateMemberCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/GenerateMember/AbstractGenerateMemberCodeFixProvider.cs index ffd9e180dff1..d62db2d5d407 100644 --- a/src/Analyzers/Core/CodeFixes/GenerateMember/AbstractGenerateMemberCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/GenerateMember/AbstractGenerateMemberCodeFixProvider.cs @@ -60,7 +60,7 @@ private IEnumerable GetTargetNodes( ISyntaxFactsService syntaxFacts, SyntaxNode root, TextSpan span, Diagnostic diagnostic) { - var token = root.FindToken(span.Start); + var token = root.FindToken(span.Start, findInsideTrivia: true); if (token.Span.IntersectsWith(span)) { var first = true; diff --git a/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.CodeAction.cs b/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.CodeAction.cs index 814681b4c53f..82c468c9253e 100644 --- a/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.CodeAction.cs +++ b/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.CodeAction.cs @@ -86,9 +86,10 @@ protected override async Task GetChangedDocumentAsync(CancellationToke var result = await CodeGenerator.AddMethodDeclarationAsync( new CodeGenerationSolutionContext( _document.Project.Solution, - new CodeGenerationContext( - afterThisLocation: _state.Location, - generateMethodBodies: _state.TypeToGenerateIn.TypeKind != TypeKind.Interface)), + new CodeGenerationContext( + afterThisLocation: _state.Location, + generateMethodBodies: _state.TypeToGenerateIn.TypeKind != TypeKind.Interface, + allowGenerationIntoHiddenCode: IsRazorSourceGeneratedDocument)), _state.TypeToGenerateIn, method, cancellationToken).ConfigureAwait(false); diff --git a/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.State.cs b/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.State.cs index 24c4323b4a19..86cb5e1d2514 100644 --- a/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.State.cs +++ b/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.State.cs @@ -10,7 +10,6 @@ using Microsoft.CodeAnalysis.CodeGeneration; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; @@ -69,7 +68,11 @@ protected async Task TryFinishInitializingStateAsync(TService service, Sem return false; } - if (!CodeGenerator.CanAdd(document.Project.Solution, TypeToGenerateIn, cancellationToken)) + var codeGenerationContext = new CodeGenerationContext( + afterThisLocation: Location, + generateMethodBodies: TypeToGenerateIn.TypeKind != TypeKind.Interface, + allowGenerationIntoHiddenCode: IsRazorSourceGeneratedDocument); + if (!CodeGenerator.CanAdd(document.Project.Solution, TypeToGenerateIn, codeGenerationContext, cancellationToken)) { return false; } diff --git a/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.cs b/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.cs index 378e204090f7..a8e44ece29bd 100644 --- a/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.cs +++ b/src/Analyzers/Core/CodeFixes/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; -using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.CodeGeneration; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -52,7 +53,10 @@ protected async ValueTask> GetActionsAsync(Document d var semanticFacts = document.Project.Solution.GetRequiredLanguageService(state.TypeToGenerateIn.Language); if (semanticFacts.SupportsParameterizedProperties && - state.InvocationExpressionOpt != null) + state.InvocationExpressionOpt != null && + // Generate Method has the Razor-specific hidden/source-generated override; properties should still + // only be offered when the destination passes the normal code-generation checks. + CodeGenerator.CanAdd(document.Project.Solution, state.TypeToGenerateIn, cancellationToken)) { var typeParameters = state.SignatureInfo.DetermineTypeParameters(cancellationToken); var returnType = await state.SignatureInfo.DetermineReturnTypeAsync(cancellationToken).ConfigureAwait(false); @@ -68,4 +72,13 @@ protected async ValueTask> GetActionsAsync(Document d return result.ToImmutableAndClear(); } + + /// + /// Checks if a document comes from the Razor source generator + /// + /// SourceGeneratedDocument.Identity is not available in the code style layer, so we can't use the existing extension method + private static bool IsRazorSourceGeneratedDocument(Document document) + => document is SourceGeneratedDocument && + document.FilePath is string filePath && + filePath.IndexOf("Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator", StringComparison.Ordinal) >= 0; } diff --git a/src/Analyzers/Core/CodeFixes/SimplifyLinqExpression/SimplifyLinqExpressionCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/SimplifyLinqExpression/SimplifyLinqExpressionCodeFixProvider.cs index 07223b269cd8..f71ad46b160d 100644 --- a/src/Analyzers/Core/CodeFixes/SimplifyLinqExpression/SimplifyLinqExpressionCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/SimplifyLinqExpression/SimplifyLinqExpressionCodeFixProvider.cs @@ -55,6 +55,25 @@ protected override async Task FixAllAsync( // 'x.Where(...)' in the above expression. var innerInvocationExpression = syntaxFacts.GetExpressionOfMemberAccessExpression(memberAccess)!; + // We originally walked an IOperation tree, not a syntax tree. So we have to unwrap any superfluous + // wrapper nodes in syntax node represented in IOpt. + while (true) + { + if (syntaxFacts.IsParenthesizedExpression(innerInvocationExpression)) + { + innerInvocationExpression = syntaxFacts.GetExpressionOfParenthesizedExpression(innerInvocationExpression); + continue; + } + + if (syntaxFacts.IsSuppressNullableWarningExpression(innerInvocationExpression)) + { + innerInvocationExpression = syntaxFacts.GetOperandOfPostfixUnaryExpression(innerInvocationExpression); + continue; + } + + break; + } + // 'x.Where' in the above expression. var innerMemberAccessExpression = syntaxFacts.GetExpressionOfInvocationExpression(innerInvocationExpression); diff --git a/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs b/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs index 3894a87ce428..e122fbc1a899 100644 --- a/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs +++ b/src/Analyzers/Core/CodeFixes/UseCollectionInitializer/AbstractUseCollectionInitializerCodeFixProvider.cs @@ -87,7 +87,7 @@ protected sealed override async Task FixAsync( editor.ReplaceNode(oldNode, newNode); - // We only need to remove the post-matches. The pre-matches are the arguments in teh object creation, which + // We only need to remove the post-matches. The pre-matches are the arguments in the object creation, which // itself got replaced above. foreach (var match in postMatches) editor.RemoveNode(match.Node, SyntaxRemoveOptions.KeepUnbalancedDirectives); diff --git a/src/CodeStyle/Core/CodeFixes/Host/Mef/CodeStyleHostLanguageServices.cs b/src/CodeStyle/Core/CodeFixes/Host/Mef/CodeStyleHostLanguageServices.cs index 75a5c586d5ff..540cd96b05ea 100644 --- a/src/CodeStyle/Core/CodeFixes/Host/Mef/CodeStyleHostLanguageServices.cs +++ b/src/CodeStyle/Core/CodeFixes/Host/Mef/CodeStyleHostLanguageServices.cs @@ -11,6 +11,7 @@ using System.Composition.Hosting; using System.Linq; using System.Reflection; +using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; @@ -28,10 +29,30 @@ private MefHostExportProvider(CompositionHost compositionContext) public static MefHostExportProvider Create(string languageName) { var assemblies = CreateAssemblies(languageName); - var compositionConfiguration = new ContainerConfiguration().WithAssemblies(assemblies); + var types = assemblies.SelectMany(GetTypesFromAssembly); + var compositionConfiguration = new ContainerConfiguration().WithParts(types); return new MefHostExportProvider(compositionConfiguration.CreateContainer()); } + /// + /// Safely gets types from an assembly, handling + /// that can occur when some types in the assembly can't be loaded + /// + private static IEnumerable GetTypesFromAssembly(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + FatalError.ReportNonFatalError(ex); + + // Return only the types that were successfully loaded + return ex.Types.Where(t => t is not null); + } + } + private static ImmutableArray CreateAssemblies(string languageName) { using var disposer = ArrayBuilder.GetInstance(out var assemblyNames); diff --git a/src/CodeStyle/Tools/CodeStyleConfigFileGenerator.csproj b/src/CodeStyle/Tools/CodeStyleConfigFileGenerator.csproj index 0fd6810eb515..6be3c6fd1952 100644 --- a/src/CodeStyle/Tools/CodeStyleConfigFileGenerator.csproj +++ b/src/CodeStyle/Tools/CodeStyleConfigFileGenerator.csproj @@ -2,6 +2,8 @@ Exe $(NetRoslyn) + + false true false false diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs index e1b1d6f5cd1f..ebf11cc20c49 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.ValueChecks.cs @@ -247,7 +247,7 @@ public static MethodInvocationInfo FromUserDefinedConditionalLogicalOperator(Bou HasAnyErrors = logicalOperator.HasAnyErrors }; - public static MethodInvocationInfo FromUserDefinedConversion(MethodSymbol operatorMethod, BoundExpression operand, bool hasAnyErrors) + public static MethodInvocationInfo FromUserDefinedOrUnionConversion(MethodSymbol operatorMethod, BoundExpression operand, bool hasAnyErrors) => new MethodInvocationInfo { MethodInfo = MethodInfo.Create(operatorMethod), @@ -1866,6 +1866,9 @@ private bool CheckEventValueKind(BoundEventAccess boundEvent, BindValueKind valu } else { + // Unsafe member access for compound assignment is checked against the accessors elsewhere. + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, eventSymbol, eventSyntax); + if (!boundEvent.IsUsableAsField) { // Dev10 reports this in addition to ERR_BadAccess, but we won't even reach this point if the event isn't accessible (caught by lookup). @@ -1909,7 +1912,12 @@ private bool CheckEventValueKind(BoundEventAccess boundEvent, BindValueKind valu private bool CheckIsValidReceiverForVariable(SyntaxNode node, BoundExpression receiver, BindValueKind kind, BindingDiagnosticBag diagnostics) { Debug.Assert(receiver != null); - return Flags.Includes(BinderFlags.ObjectInitializerMember) && receiver.Kind == BoundKind.ObjectOrCollectionValuePlaceholder || + // Binding object initializer field/property access needs object initializer specific diagnostics: + // 1) CS1914 (ERR_StaticMemberInObjectInitializer) + // 2) CS1917 (ERR_ReadonlyValueTypeInObjectInitializer) + // 3) CS1918 (ERR_ValueTypePropertyInObjectInitializer) + // These only apply on the left side of an object initializer member assignment, not the RHS. + return (receiver.Kind == BoundKind.ObjectOrCollectionValuePlaceholder && IsObjectInitializerMemberTarget(node)) || CheckValueKind(node, receiver, kind, true, diagnostics); } @@ -2074,6 +2082,7 @@ private bool CheckPropertyValueKind(SyntaxNode node, BoundExpression expr, BindV } ReportDiagnosticsIfObsolete(diagnostics, setMethod, node, receiver?.Kind == BoundKind.BaseReference); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, setMethod, node); var setValueKind = setMethod.IsEffectivelyReadOnly ? BindValueKind.RValue : BindValueKind.Assignable; if (RequiresVariableReceiver(receiver, setMethod) && !CheckIsValidReceiverForVariable(node, receiver, setValueKind, diagnostics)) @@ -2124,6 +2133,7 @@ private bool CheckPropertyValueKind(SyntaxNode node, BoundExpression expr, BindV CheckImplicitThisCopyInReadOnlyMember(receiver, getMethod, diagnostics); ReportDiagnosticsIfObsolete(diagnostics, getMethod, node, receiver?.Kind == BoundKind.BaseReference); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, getMethod, node); if (IsBadBaseAccess(node, receiver, getMethod, diagnostics, propertySymbol) || reportUseSite(getMethod)) @@ -3929,7 +3939,7 @@ internal SafeContext GetRefEscape(BoundExpression expr) case BoundKind.Conversion: Debug.Assert(expr is BoundConversion conversion && - (!conversion.Conversion.IsUserDefined || + ((!conversion.Conversion.IsUserDefined && !conversion.Conversion.IsUnion) || conversion.Conversion.Method.HasUnsupportedMetadata || conversion.Conversion.Method.RefKind == RefKind.None)); break; @@ -4267,7 +4277,7 @@ internal bool CheckRefEscape(SyntaxNode node, BoundExpression expr, SafeContext return CheckRefEscape(node, conversion.Operand, escapeTo, checkingReceiver, diagnostics); } - Debug.Assert(!conversion.Conversion.IsUserDefined || + Debug.Assert((!conversion.Conversion.IsUserDefined && !conversion.Conversion.IsUnion) || conversion.Conversion.Method.HasUnsupportedMetadata || conversion.Conversion.Method.RefKind == RefKind.None); break; @@ -4615,13 +4625,13 @@ internal SafeContext GetValEscape(BoundExpression expr) isRefEscape: false); } - if (conversion.Conversion.IsUserDefined) + if (conversion.Conversion is { IsUserDefined: true } or { IsUnion: true }) { var operatorMethod = conversion.Conversion.Method; Debug.Assert(operatorMethod is not null); return GetInvocationEscapeScope( - MethodInvocationInfo.FromUserDefinedConversion(operatorMethod, conversion.Operand, conversion.HasAnyErrors), + MethodInvocationInfo.FromUserDefinedOrUnionConversion(operatorMethod, conversion.Operand, conversion.HasAnyErrors), isRefEscape: false); } @@ -5363,14 +5373,14 @@ internal bool CheckValEscape(SyntaxNode node, BoundExpression expr, SafeContext isRefEscape: false); } - if (conversion.Conversion.IsUserDefined) + if (conversion.Conversion is { IsUserDefined: true } or { IsUnion: true }) { var operatorMethod = conversion.Conversion.Method; Debug.Assert(operatorMethod is not null); return CheckInvocationEscape( conversion.Syntax, - MethodInvocationInfo.FromUserDefinedConversion(operatorMethod, conversion.Operand, conversion.HasAnyErrors), + MethodInvocationInfo.FromUserDefinedOrUnionConversion(operatorMethod, conversion.Operand, conversion.HasAnyErrors), checkingReceiver, escapeTo, diagnostics, diff --git a/src/Compilers/CSharp/Portable/Binder/Binder.cs b/src/Compilers/CSharp/Portable/Binder/Binder.cs index ba2d6382760d..8238d60079da 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder.cs @@ -78,6 +78,37 @@ internal bool IsEarlyAttributeBinder internal virtual bool IsInsideNameof => NextRequired.IsInsideNameof; + internal static bool IsObjectInitializerMemberTarget(SyntaxNode node) + { + while (node.Parent is { } parent) + { + switch (parent) + { + case AssignmentExpressionSyntax assignment: + return assignment.Left == node && + assignment.Parent?.Kind() == SyntaxKind.ObjectInitializerExpression; + + case InitializerExpressionSyntax initializer + when node is IdentifierNameSyntax && + initializer.Kind() == SyntaxKind.ObjectInitializerExpression: + return true; + + case BracketedArgumentListSyntax: + // We cut off inside the indexer argument list of an object initializer so + // things like "new C().StaticProp" get standard error messages, rather than + // the object initializer specific error CS1914. + return false; + + case StatementSyntax: + return false; + } + + node = parent; + } + + return false; + } + /// /// Get the next binder in which to look up a name, if not found by this binder. /// diff --git a/src/Compilers/CSharp/Portable/Binder/BinderFlags.cs b/src/Compilers/CSharp/Portable/Binder/BinderFlags.cs index 5a67fe48ff74..a42e86e1edea 100644 --- a/src/Compilers/CSharp/Portable/Binder/BinderFlags.cs +++ b/src/Compilers/CSharp/Portable/Binder/BinderFlags.cs @@ -19,7 +19,7 @@ internal enum BinderFlags : uint SuppressObsoleteChecks = 1 << 1, ConstructorInitializer = 1 << 2, FieldInitializer = 1 << 3, - ObjectInitializerMember = 1 << 4, // object initializer field/property access + // Previously used, can be reused in the future CollectionInitializerAddMethod = 1 << 5, // used for collection initializer add method overload resolution diagnostics AttributeArgument = 1 << 6, GenericConstraintsClause = 1 << 7, // "where" clause (used for cycle checking) diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Attributes.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Attributes.cs index b06635bcc4ed..c09eb701062c 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Attributes.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Attributes.cs @@ -529,6 +529,7 @@ private BoundAssignmentOperator BindNamedAttributeArgument(AttributeArgumentSynt LookupResultKind resultKind; Symbol namedArgumentNameSymbol = BindNamedAttributeArgumentName(namedArgument, attributeType, diagnostics, out wasError, out resultKind); ReportDiagnosticsIfObsolete(diagnostics, namedArgumentNameSymbol, namedArgument, hasBaseReceiver: false); + // Unsafe property access is checked on the accessor only to avoid duplicate diagnostics. if (namedArgumentNameSymbol.Kind == SymbolKind.Property) { @@ -537,6 +538,7 @@ private BoundAssignmentOperator BindNamedAttributeArgument(AttributeArgumentSynt if (setMethod != null) { ReportDiagnosticsIfObsolete(diagnostics, setMethod, namedArgument, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, setMethod, namedArgument); if (setMethod.IsInitOnly && setMethod.DeclaringCompilation != this.Compilation) { @@ -545,6 +547,10 @@ private BoundAssignmentOperator BindNamedAttributeArgument(AttributeArgumentSynt } } } + else + { + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, namedArgumentNameSymbol, namedArgument); + } Debug.Assert(resultKind == LookupResultKind.Viable || wasError); diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Await.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Await.cs index ef9417f95605..c1ee4087d71e 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Await.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Await.cs @@ -403,7 +403,7 @@ bool tryGetRuntimeAwaitHelper(BoundExpression expression, out BoundAwaitableValu return false; } - reportObsoleteDiagnostics(this, diagnostics, runtimeAwaitCall.Method, expression.Syntax); + reportObsoleteAndUnsafeDiagnostics(this, diagnostics, runtimeAwaitCall.Method, expression.Syntax); return true; static bool isApplicableMethod( @@ -515,7 +515,7 @@ bool getRuntimeAwaitAwaiter(TypeSymbol awaiterType, out BoundCall? runtimeAwaitA runtimeAwaitAwaiterMethod, new ConstraintsHelper.CheckConstraintsArgs(this.Compilation, this.Conversions, includeNullability: false, syntax.Location, diagnostics)); - reportObsoleteDiagnostics(this, diagnostics, runtimeAwaitAwaiterMethod, syntax); + reportObsoleteAndUnsafeDiagnostics(this, diagnostics, runtimeAwaitAwaiterMethod, syntax); placeholder = new BoundAwaitableValuePlaceholder(syntax, awaiterType); @@ -541,10 +541,11 @@ bool getRuntimeAwaitAwaiter(TypeSymbol awaiterType, out BoundCall? runtimeAwaitA return true; } - static void reportObsoleteDiagnostics(Binder @this, BindingDiagnosticBag diagnostics, MethodSymbol method, SyntaxNode syntax) + static void reportObsoleteAndUnsafeDiagnostics(Binder @this, BindingDiagnosticBag diagnostics, MethodSymbol method, SyntaxNode syntax) { @this.ReportDiagnosticsIfObsolete(diagnostics, method, syntax, hasBaseReceiver: false); @this.ReportDiagnosticsIfObsolete(diagnostics, method.ContainingType, syntax, hasBaseReceiver: false); + @this.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, method, syntax); } } @@ -644,6 +645,13 @@ private bool GetIsCompletedProperty(TypeSymbol awaiterType, SyntaxNode node, Typ return false; } + qualified = CheckValue(qualified, BindValueKind.RValue, diagnostics); + if (qualified.HasAnyErrors) + { + isCompletedProperty = null; + return false; + } + isCompletedProperty = propertySymbol; if (isCompletedProperty.IsWriteOnly) { diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs index edbde47e2f77..7fb2f2b593e0 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Conversions.cs @@ -71,24 +71,61 @@ protected BoundExpression CreateConversion( #if DEBUG if (source is BoundValuePlaceholder placeholder1) { - Debug.Assert(filterConversion(conversion)); + Debug.Assert(filterConversion(conversion, result)); Debug.Assert(BoundNode.GetConversion(result, placeholder1) == conversion); } - else if (source.Type is not null && filterConversion(conversion)) + else if (source.Type is not null && filterConversion(conversion, result)) { var placeholder2 = new BoundValuePlaceholder(source.Syntax, source.Type); var result2 = createConversion(syntax, placeholder2, conversion, isCast, conversionGroupOpt: new ConversionGroup(conversion), InConversionGroupFlags.Unspecified, wasCompilerGenerated, destination, BindingDiagnosticBag.Discarded, hasErrors); Debug.Assert(BoundNode.GetConversion(result2, placeholder2) == conversion); } - static bool filterConversion(Conversion conversion) + static bool filterConversion(Conversion conversion, BoundExpression result) { - return !conversion.IsInterpolatedString && - !conversion.IsInterpolatedStringHandler && - !conversion.IsSwitchExpression && - !conversion.IsCollectionExpression && - !(conversion.IsTupleLiteralConversion || (conversion.IsNullable && conversion.UnderlyingConversions[0].IsTupleLiteralConversion)) && - (!conversion.IsUserDefined || filterConversion(conversion.UserDefinedFromConversion)); + if (conversion.IsInterpolatedString) + { + return false; + } + + if (conversion.IsInterpolatedStringHandler) + { + return false; + } + + if (conversion.IsSwitchExpression) + { + return false; + } + + if (conversion.IsCollectionExpression) + { + return false; + } + + if ((conversion.IsTupleLiteralConversion || (conversion.IsNullable && conversion.UnderlyingConversions[0].IsTupleLiteralConversion))) + { + return false; + } + + if (conversion.IsUserDefined && !filterConversion(conversion.UserDefinedFromConversion, result)) + { + return false; + } + + if (conversion.IsUnion && !filterConversion(conversion.BestUnionConversionAnalysis.SourceConversion, result)) + { + return false; + } + + if ((result as BoundConversion)?.ConversionGroupOpt?.Conversion.IsUnion == true && + !conversion.IsUnion && + conversion != ((BoundConversion)result).ConversionGroupOpt!.Conversion.BestUnionConversionAnalysis!.SourceConversion) + { + return false; + } + + return true; } #endif @@ -274,6 +311,13 @@ BoundExpression createConversion( return CreateUserDefinedConversion(syntax, source, conversion, isCast: isCast, conversionGroupOpt ?? new ConversionGroup(conversion), destination, diagnostics, hasErrors); } + if (conversion.IsUnion) + { + // Union conversions are likely to be represented as multiple + // BoundConversion instances so a ConversionGroup is necessary. + return CreateUnionConversion(syntax, source, conversion, isCast: isCast, conversionGroupOpt ?? new ConversionGroup(conversion), destination, diagnostics); + } + ConstantValue? constantValue = this.FoldConstantConversion(syntax, source, conversion, destination, diagnostics); if (conversion.Kind == ConversionKind.DefaultLiteral) { @@ -319,6 +363,7 @@ void reportUseSiteDiagnostics(SyntaxNode syntax, Conversion conversion, BoundExp ReportDiagnosticsIfObsolete(diagnostics, conversion, syntax, hasBaseReceiver: false); if (conversion.Method is not null) { + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, conversion.Method, syntax); ReportUseSite(conversion.Method, diagnostics, syntax.Location); } @@ -453,6 +498,10 @@ void checkConstraintLanguageVersionAndRuntimeSupportForConversion(SyntaxNode syn } } } + else if (conversion.IsUnion) + { + CheckFeatureAvailability(syntax, MessageID.IDS_FeatureUnions, diagnostics); + } else if (conversion.IsInlineArray) { if (!Compilation.Assembly.RuntimeSupportsInlineArrayTypes) @@ -468,6 +517,7 @@ void checkConstraintLanguageVersionAndRuntimeSupportForConversion(SyntaxNode syn Debug.Assert(elementField is { }); diagnostics.ReportUseSite(elementField, syntax); + AssertNotUnsafeMemberAccess(elementField); // https://github.com/dotnet/roslyn/issues/82546: Support unsafe fields? if (destination.OriginalDefinition.Equals(Compilation.GetWellKnownType(WellKnownType.System_ReadOnlySpan_T), TypeCompareKind.AllIgnoreOptions)) { @@ -1569,6 +1619,7 @@ internal void CheckCollectionBuilderMethod( ReportDiagnosticsIfObsolete(diagnostics, collectionBuilderMethod.ContainingType, syntax, hasBaseReceiver: false); ReportDiagnosticsIfObsolete(diagnostics, collectionBuilderMethod, syntax, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, collectionBuilderMethod, syntax); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, collectionBuilderMethod, syntax, isDelegateConversion: false); Debug.Assert(!collectionBuilderMethod.IsExtensionBlockMember()); @@ -1701,6 +1752,7 @@ static void bindClassCreationExpressionContinued( var method = memberResolutionResult.Member; binder.ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); + binder.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, method, node); // NOTE: Use-site diagnostics were reported during overload resolution. ImmutableSegmentedDictionary requiredMembers = GetMembersRequiringInitialization(method); @@ -1950,6 +2002,7 @@ static bool bindMethodGroupInvocation( else if (addMethods.Length == 1) { addMethodBinder.ReportDiagnosticsIfObsolete(diagnostics, addMethods[0], syntax, hasBaseReceiver: false); + addMethodBinder.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, addMethods[0], syntax); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, addMethods[0], syntax, isDelegateConversion: false); Debug.Assert(!IsDisallowedExtensionInOlderLangVer(addMethods[0])); } @@ -2426,7 +2479,7 @@ private BoundExpression ConvertSwitchExpression(BoundUnconvertedSwitchExpression ? CreateConversion(oldValue.Syntax, oldValue, underlyingConversions[i], isCast: false, conversionGroupOpt: null, InConversionGroupFlags.Unspecified, destination, diagnostics) : GenerateConversionForAssignment(destination, oldValue, diagnostics); var newCase = (oldValue == newValue) ? oldCase : - new BoundSwitchExpressionArm(oldCase.Syntax, oldCase.Locals, oldCase.Pattern, oldCase.WhenClause, newValue, oldCase.Label, oldCase.HasErrors); + new BoundSwitchExpressionArm(oldCase.Syntax, oldCase.Locals, oldCase.Pattern, oldCase.HasUnionMatching, oldCase.WhenClause, newValue, oldCase.Label, oldCase.HasErrors); builder.Add(newCase); } conversion.MarkUnderlyingConversionsChecked(); @@ -2626,6 +2679,83 @@ private BoundExpression CreateUserDefinedConversion( return finalConversion; } + private BoundExpression CreateUnionConversion( + SyntaxNode syntax, + BoundExpression source, + Conversion conversion, + bool isCast, + ConversionGroup conversionGroup, + TypeSymbol destination, + BindingDiagnosticBag diagnostics) + { + Debug.Assert(conversionGroup != null); + Debug.Assert(conversionGroup.Conversion == conversion); + Debug.Assert(conversion.IsUnion); + Debug.Assert(conversion.IsValid); + Debug.Assert(conversion.BestUnionConversionAnalysis is object); // All valid union conversions have this populated + + UserDefinedConversionAnalysis analysis = conversion.BestUnionConversionAnalysis; + + Debug.Assert(analysis.Kind == UserDefinedConversionAnalysisKind.ApplicableInNormalForm); + Debug.Assert(analysis.Operator is { MethodKind: MethodKind.Constructor, ParameterCount: 1 }); + Debug.Assert(TypeSymbol.Equals(analysis.FromType, analysis.Operator.GetParameterType(0), TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(TypeSymbol.Equals(destination.StrippedType(), analysis.Operator.ContainingType, TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(TypeSymbol.Equals(destination.StrippedType(), analysis.ToType, TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(analysis.TargetConversion is { IsIdentity: true } or { IsNullable: true, IsImplicit: true }); + + conversion.MarkUnderlyingConversionsChecked(); + + // Original expression --> conversion's "from" type + BoundExpression convertedOperand = CreateConversion( + syntax: syntax, + source: source, + conversion: analysis.SourceConversion, + isCast: false, + conversionGroupOpt: conversionGroup, + InConversionGroupFlags.UnionSourceConversion, + wasCompilerGenerated: true, + destination: analysis.FromType, + diagnostics: diagnostics); + + if (analysis.Operator.ContainingType.IsAbstract) + { + // Report error for new of abstract type. + diagnostics.Add(ErrorCode.ERR_NoNewAbstract, syntax.Location, analysis.Operator.ContainingType); + } + + // https://github.com/dotnet/roslyn/issues/82636: Any other validations to perform? Perhaps we should simply bind object creation, drop the node, but keep diagnostics. + + var unionConversion = new BoundConversion( + syntax, + convertedOperand, + conversion, + @checked: CheckOverflowAtRuntime, + explicitCastInCode: isCast, + conversionGroup, + InConversionGroupFlags.UnionConstructor, + constantValueOpt: ConstantValue.NotAvailable, + type: analysis.ToType) + { WasCompilerGenerated = true }; + + // Conversion's "to" type --> final type + BoundExpression finalConversion = CreateConversion( + syntax: syntax, + source: unionConversion, + conversion: analysis.TargetConversion, + isCast: false, + conversionGroupOpt: conversionGroup, + InConversionGroupFlags.UnionFinal, + wasCompilerGenerated: true, // NOTE: doesn't necessarily set flag on resulting bound expression. + destination: destination, + diagnostics: diagnostics); + + conversion.AssertUnderlyingConversionsCheckedRecursive(); + + finalConversion.ResetCompilerGenerated(source.WasCompilerGenerated); + + return finalConversion; + } + private BoundExpression CreateFunctionTypeConversion( SyntaxNode syntax, BoundExpression source, Conversion conversion, bool isCast, ConversionGroup? conversionGroup, InConversionGroupFlags inConversionGroupFlags, @@ -2893,10 +3023,19 @@ private BoundExpression CreateStackAllocConversion( switch (conversion.Kind) { case ConversionKind.StackAllocToPointerType: - ReportUnsafeIfNotAllowed(syntax.Location, diagnostics); + ReportUnsafeIfNotAllowed(syntax.Location, diagnostics, disallowedUnder: MemorySafetyRules.Legacy); stackAllocType = new PointerTypeSymbol(TypeWithAnnotations.Create(elementType)); break; case ConversionKind.StackAllocToSpanType: + // Under the updated memory safety rules, a stackalloc_expression is unsafe if being converted to Span/ROS, + // does not have an initializer, and is used within a member with SkipLocalsInitAttribute. + // https://github.com/dotnet/roslyn/issues/82546: Confirm this rule with LDM. + if (boundStackAlloc.InitializerOpt is null && + ContainingMemberOrLambda is MethodSymbol { AreLocalsZeroed: false }) + { + ReportUnsafeIfNotAllowed(syntax, diagnostics, disallowedUnder: MemorySafetyRules.Updated, customErrorCode: ErrorCode.ERR_UnsafeUninitializedStackAlloc); + } + CheckFeatureAvailability(syntax, MessageID.IDS_FeatureRefStructs, diagnostics); stackAllocType = Compilation.GetWellKnownType(WellKnownType.System_Span_T).Construct(elementType); break; @@ -3526,7 +3665,7 @@ private bool MethodGroupConversionHasErrors( } if ((selectedMethod.HasParameterContainingPointerType() || selectedMethod.ReturnType.ContainsPointerOrFunctionPointer()) - && ReportUnsafeIfNotAllowed(syntax, diagnostics)) + && ReportUnsafeIfNotAllowed(syntax, diagnostics, disallowedUnder: MemorySafetyRules.Legacy)) { return true; } @@ -3537,6 +3676,7 @@ private bool MethodGroupConversionHasErrors( ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, selectedMethod, syntax, isDelegateConversion: true); } ReportDiagnosticsIfObsolete(diagnostics, selectedMethod, syntax, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, selectedMethod, syntax); ReportDiagnosticsIfDisallowedExtension(diagnostics, selectedMethod, syntax); // No use site errors, but there could be use site warnings. diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs index 4be320e5531a..d2e69288281c 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Expressions.cs @@ -602,7 +602,7 @@ private void CheckContextForPointerTypes(ExpressionSyntax node, BindingDiagnosti TypeSymbol exprType = expr.Type; if ((object)exprType != null && exprType.ContainsPointerOrFunctionPointer()) { - ReportUnsafeIfNotAllowed(node, diagnostics); + ReportUnsafeIfNotAllowed(node, diagnostics, disallowedUnder: MemorySafetyRules.Legacy); //CONSIDER: Return a bad expression so that HasErrors is true? } } @@ -1469,7 +1469,7 @@ private BoundExpression BindSizeOf(SizeOfExpressionSyntax node, BindingDiagnosti BoundTypeExpression boundType = new BoundTypeExpression(typeSyntax, alias, typeWithAnnotations, typeHasErrors); ConstantValue constantValue = GetConstantSizeOf(type); - bool hasErrors = constantValue is null && ReportUnsafeIfNotAllowed(node, diagnostics, type); + bool hasErrors = constantValue is null && ReportUnsafeIfNotAllowed(node, diagnostics, sizeOfTypeOpt: type, disallowedUnder: MemorySafetyRules.Legacy); return new BoundSizeOfOperator(node, boundType, constantValue, this.GetSpecialType(SpecialType.System_Int32, diagnostics, node), hasErrors); } @@ -2054,10 +2054,11 @@ private bool IsBadLocalOrParameterCapture(Symbol symbol, TypeSymbol type, RefKin private BoundExpression BindNonMethod(SimpleNameSyntax node, Symbol symbol, BindingDiagnosticBag diagnostics, LookupResultKind resultKind, bool indexed, bool isError) { - // Events are handled later as we don't know yet if we are binding to the event or it's backing field. + // Events are handled later as we don't know yet if we are binding to the event or its backing field. if (symbol.Kind is not (SymbolKind.Event or SymbolKind.Property)) { ReportDiagnosticsIfObsolete(diagnostics, symbol, node, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, symbol, node); } switch (symbol.Kind) @@ -3724,7 +3725,7 @@ void reportUnsafeIfNeeded(MemberResolutionResult methodResult, BindingD if (!methodResult.Member.IsIndexer() && !argument.HasAnyErrors && parameterTypeWithAnnotations.Type.ContainsPointerOrFunctionPointer()) { // CONSIDER: dev10 uses the call syntax, but this seems clearer. - ReportUnsafeIfNotAllowed(argument.Syntax, diagnostics); + ReportUnsafeIfNotAllowed(argument.Syntax, diagnostics, disallowedUnder: MemorySafetyRules.Legacy); //CONSIDER: Return a bad expression so that HasErrors is true? } } @@ -5102,10 +5103,11 @@ private BoundExpression BindConstructorInitializerCoreContinued( // Don't worry about double reporting (i.e. for both the argument and the parameter) // because only one unsafe diagnostic is allowed per scope - the others are suppressed. - hasErrors = ReportUnsafeIfNotAllowed(errorLocation, diagnostics); + hasErrors = ReportUnsafeIfNotAllowed(errorLocation, diagnostics, disallowedUnder: MemorySafetyRules.Legacy); } ReportDiagnosticsIfObsolete(diagnostics, resultMember, nonNullSyntax, hasBaseReceiver: isBaseConstructorInitializer); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, resultMember, nonNullSyntax); var expanded = memberResolutionResult.Result.Kind == MemberResolutionKind.ApplicableInExpandedForm; @@ -5400,6 +5402,8 @@ static BoundNode bindSpreadElement(SpreadElementSyntax syntax, BindingDiagnostic hasErrors); } + builder.ReportDiagnosticsIfUnsafeMemberAccess(@this, syntax.OperatorToken, syntax, diagnostics); + Debug.Assert(expression.Type is { }); var expressionPlaceholder = new BoundCollectionExpressionSpreadExpressionPlaceholder(syntax.Expression, expression.Type); @@ -5787,13 +5791,12 @@ private BoundObjectInitializerExpressionBase BindInitializerExpression( switch (syntax.Kind()) { case SyntaxKind.ObjectInitializerExpression: - // Uses a special binder to produce customized diagnostics for the object initializer return BindObjectInitializerExpression( - syntax, type, diagnostics, implicitReceiver, useObjectInitDiagnostics: true); + syntax, type, diagnostics, implicitReceiver); case SyntaxKind.WithInitializerExpression: return BindObjectInitializerExpression( - syntax, type, diagnostics, implicitReceiver, useObjectInitDiagnostics: false); + syntax, type, diagnostics, implicitReceiver); case SyntaxKind.CollectionInitializerExpression: return BindCollectionInitializerExpression(syntax, type, diagnostics, implicitReceiver); @@ -5830,8 +5833,7 @@ private BoundObjectInitializerExpression BindObjectInitializerExpression( InitializerExpressionSyntax initializerSyntax, TypeSymbol initializerType, BindingDiagnosticBag diagnostics, - BoundObjectOrCollectionValuePlaceholder implicitReceiver, - bool useObjectInitDiagnostics) + BoundObjectOrCollectionValuePlaceholder implicitReceiver) { // SPEC: 7.6.10.2 Object initializers // @@ -5846,16 +5848,6 @@ private BoundObjectInitializerExpression BindObjectInitializerExpression( if (initializerSyntax.Kind() == SyntaxKind.ObjectInitializerExpression) MessageID.IDS_FeatureObjectInitializer.CheckFeatureAvailability(diagnostics, initializerSyntax.OpenBraceToken); - // We use a location specific binder for binding object initializer field/property access to generate object initializer specific diagnostics: - // 1) CS1914 (ERR_StaticMemberInObjectInitializer) - // 2) CS1917 (ERR_ReadonlyValueTypeInObjectInitializer) - // 3) CS1918 (ERR_ValueTypePropertyInObjectInitializer) - // Note that this is only used for the LHS of the assignment - these diagnostics do not apply on the RHS. - // For this reason, we will actually need two binders: this and this.WithAdditionalFlags. - var objectInitializerMemberBinder = useObjectInitDiagnostics - ? this.WithAdditionalFlags(BinderFlags.ObjectInitializerMember) - : this; - var initializers = ArrayBuilder.GetInstance(initializerSyntax.Expressions.Count); // Member name map to report duplicate assignments to a field/property. @@ -5863,7 +5855,7 @@ private BoundObjectInitializerExpression BindObjectInitializerExpression( foreach (var memberInitializer in initializerSyntax.Expressions) { BoundExpression boundMemberInitializer = BindInitializerMemberAssignment( - memberInitializer, objectInitializerMemberBinder, diagnostics, implicitReceiver); + memberInitializer, diagnostics, implicitReceiver); initializers.Add(boundMemberInitializer); @@ -5879,7 +5871,6 @@ private BoundObjectInitializerExpression BindObjectInitializerExpression( private BoundExpression BindInitializerMemberAssignment( ExpressionSyntax memberInitializer, - Binder objectInitializerMemberBinder, BindingDiagnosticBag diagnostics, BoundObjectOrCollectionValuePlaceholder implicitReceiver) { @@ -5891,15 +5882,7 @@ private BoundExpression BindInitializerMemberAssignment( { var initializer = (AssignmentExpressionSyntax)memberInitializer; - // We use a location specific binder for binding object initializer field/property access to generate object initializer specific diagnostics: - // 1) CS1914 (ERR_StaticMemberInObjectInitializer) - // 2) CS1917 (ERR_ReadonlyValueTypeInObjectInitializer) - // 3) CS1918 (ERR_ValueTypePropertyInObjectInitializer) - // See comments in BindObjectInitializerExpression for more details. - - Debug.Assert(objectInitializerMemberBinder != null); - - BoundExpression boundLeft = objectInitializerMemberBinder.BindObjectInitializerMember(initializer, implicitReceiver, diagnostics); + BoundExpression boundLeft = BindObjectInitializerMember(initializer, implicitReceiver, diagnostics); if (boundLeft != null) { @@ -5931,9 +5914,7 @@ private BoundExpression BindInitializerMemberAssignment( Error(diagnostics, ErrorCode.ERR_InvalidInitializerElementInitializer, memberInitializer); var identifierName = (IdentifierNameSyntax)memberInitializer; - Debug.Assert(objectInitializerMemberBinder != null); - - var boundNode = objectInitializerMemberBinder.BindObjectInitializerMemberMissingAssignment(identifierName, implicitReceiver, diagnostics); + var boundNode = BindObjectInitializerMemberMissingAssignment(identifierName, implicitReceiver, diagnostics); var badRight = new BoundBadExpression( identifierName, @@ -6136,7 +6117,6 @@ private BoundExpression BindObjectInitializerMemberCommon( if (handlerPlaceholders.Any(static placeholder => placeholder.ArgumentIndex is BoundInterpolatedStringArgumentPlaceholder.InstanceParameter or BoundInterpolatedStringArgumentPlaceholder.ExtensionReceiver)) { diagnostics.Add(ErrorCode.ERR_InterpolatedStringsReferencingInstanceCannotBeInObjectInitializers, argument.Syntax.Location); - hasErrors = true; } } } @@ -6443,8 +6423,7 @@ private BoundCollectionInitializerExpression BindCollectionInitializerExpression // NOTE: collectionInitializerAddMethodBinder is used only for binding the Add method invocation expression, but not the entire initializer. // NOTE: Hence it is being passed as a parameter to BindCollectionInitializerElement(). // NOTE: Ideally we would want to avoid this and bind the entire initializer with the collectionInitializerAddMethodBinder. - // NOTE: However, this approach has few issues. These issues also occur when binding object initializer member assignment. - // NOTE: See comments for objectInitializerMemberBinder in BindObjectInitializerExpression method for details about the pitfalls of alternate approaches. + // NOTE: However, this approach has few issues. These same issues also occur when binding object initializer member assignments. BoundExpression boundElementInitializer = BindCollectionInitializerElement(elementInitializer, initializerType, hasEnumerableInitializerType, collectionInitializerAddMethodBinder, diagnostics, implicitReceiver); @@ -6974,10 +6953,11 @@ private BoundObjectCreationExpression BindClassCreationExpressionContinued( { // Don't worry about double reporting (i.e. for both the argument and the parameter) // because only one unsafe diagnostic is allowed per scope - the others are suppressed. - hasError = ReportUnsafeIfNotAllowed(node, diagnostics) || hasError; + hasError = ReportUnsafeIfNotAllowed(node, diagnostics, disallowedUnder: MemorySafetyRules.Legacy) || hasError; } ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, method, node); // NOTE: Use-site diagnostics were reported during overload resolution. ConstantValue constantValueOpt = (initializerSyntaxOpt == null && method.IsDefaultValueTypeConstructor()) ? @@ -7658,6 +7638,11 @@ private BoundExpression BindMemberAccess( { WasCompilerGenerated = true, // don't interfere with the type info for exprSyntax. }; + + if (!boundLeft.HasErrors) + { + ReportUnsafeIfNotAllowed(node.OperatorToken.GetLocation(), diagnostics, MemorySafetyRules.Updated); + } } } @@ -8074,6 +8059,7 @@ BoundExpression tryBindMemberAccessWithBoundNamespaceLeft( } ReportDiagnosticsIfObsolete(diagnostics, type, node, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, type, node); return new BoundTypeExpression(node, null, type); } @@ -8678,11 +8664,12 @@ private BoundExpression BindMemberOfType( Debug.Assert(symbol.Kind != SymbolKind.Method); left = ReplaceTypeOrValueReceiver(left, symbol.IsStatic || symbol.Kind == SymbolKind.NamedType, diagnostics); - // Events are handled later as we don't know yet if we are binding to the event or it's backing field. + // Events are handled later as we don't know yet if we are binding to the event or its backing field. // Properties are handled in BindPropertyAccess if (symbol.Kind is not (SymbolKind.Event or SymbolKind.Property)) { ReportDiagnosticsIfObsolete(diagnostics, symbol, node, hasBaseReceiver: left.Kind == BoundKind.BaseReference); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, symbol, node); } switch (symbol.Kind) @@ -9300,6 +9287,11 @@ private BoundExpression BindPropertyAccess( { ReportDiagnosticsIfObsolete(diagnostics, propertySymbol, node, hasBaseReceiver: receiver?.Kind == BoundKind.BaseReference); + // Unsafe member access is checked on the accessor only to avoid duplicate diagnostics. + Debug.Assert(propertySymbol.CallerUnsafeMode == CallerUnsafeMode.None || + (propertySymbol.GetMethod is null || propertySymbol.GetMethod.CallerUnsafeMode == propertySymbol.CallerUnsafeMode) || + (propertySymbol.SetMethod is null || propertySymbol.SetMethod.CallerUnsafeMode == propertySymbol.CallerUnsafeMode)); + bool hasError = this.CheckInstanceOrStatic(node, receiver, propertySymbol, ref lookupResult, diagnostics); if (!propertySymbol.IsStatic) @@ -9383,6 +9375,11 @@ private BoundExpression BindEventAccess( bool isUsableAsField = eventSymbol.HasAssociatedField && this.IsAccessible(eventSymbol.AssociatedField, ref useSiteInfo, (receiver != null) ? receiver.Type : null); diagnostics.Add(node, useSiteInfo); + // Unsafe member access is checked on the accessor only to avoid duplicate diagnostics. + Debug.Assert(eventSymbol.CallerUnsafeMode == CallerUnsafeMode.None || + (eventSymbol.AddMethod is null || eventSymbol.AddMethod.CallerUnsafeMode == eventSymbol.CallerUnsafeMode) || + (eventSymbol.RemoveMethod is null || eventSymbol.RemoveMethod.CallerUnsafeMode == eventSymbol.CallerUnsafeMode)); + bool hasError = this.CheckInstanceOrStatic(node, receiver, eventSymbol, ref lookupResult, diagnostics); if (!eventSymbol.IsStatic) @@ -9432,7 +9429,12 @@ private bool CheckInstanceOrStatic( { if (!IsInsideNameof) { - ErrorCode errorCode = this.Flags.Includes(BinderFlags.ObjectInitializerMember) ? + // Binding object initializer field/property access needs object initializer specific diagnostics: + // 1) CS1914 (ERR_StaticMemberInObjectInitializer) + // 2) CS1917 (ERR_ReadonlyValueTypeInObjectInitializer) + // 3) CS1918 (ERR_ValueTypePropertyInObjectInitializer) + // These only apply on the left side of an object initializer member assignment, not the RHS. + ErrorCode errorCode = IsObjectInitializerMemberTarget(node) ? ErrorCode.ERR_StaticMemberInObjectInitializer : ErrorCode.ERR_ObjectProhibited; Error(diagnostics, errorCode, node, symbol); @@ -9590,7 +9592,15 @@ private static bool IsMethodOrPropertyGroup(ArrayBuilder members) private BoundExpression BindElementAccess(ElementAccessExpressionSyntax node, BindingDiagnosticBag diagnostics) { BoundExpression receiver = BindExpression(node.Expression, diagnostics: diagnostics, invoked: false, indexed: true); - return BindElementAccess(node, receiver, node.ArgumentList, allowInlineArrayElementAccess: true, diagnostics); + var result = BindElementAccess(node, receiver, node.ArgumentList, allowInlineArrayElementAccess: true, diagnostics); + + if (!result.HasErrors && receiver.Type?.IsPointerOrFunctionPointer() == true) + { + Debug.Assert(receiver.Type?.IsFunctionPointer() != true, "There should have been an error reported for indexing into a function pointer."); + ReportUnsafeIfNotAllowed(node.ArgumentList.OpenBracketToken.GetLocation(), diagnostics, MemorySafetyRules.Updated); + } + + return result; } private BoundExpression BindElementAccess(ExpressionSyntax node, BoundExpression receiver, BracketedArgumentListSyntax argumentList, bool allowInlineArrayElementAccess, BindingDiagnosticBag diagnostics) @@ -9809,6 +9819,7 @@ BoundExpression bindInlineArrayElementAccess(ExpressionSyntax node, BoundExpress CheckFeatureAvailability(node, MessageID.IDS_FeatureInlineArrays, diagnostics); diagnostics.ReportUseSite(elementField, node); + AssertNotUnsafeMemberAccess(elementField); // https://github.com/dotnet/roslyn/issues/82546: Support unsafe fields? TypeSymbol resultType; @@ -10464,6 +10475,7 @@ private BoundExpression BindIndexerOrIndexedPropertyAccessContinued( PropertySymbol property = resolutionResult.Member; ReportDiagnosticsIfObsolete(diagnostics, property, syntax, hasBaseReceiver: receiver != null && receiver.Kind == BoundKind.BaseReference); + // Unsafe member access is checked on the accessor only to avoid duplicate diagnostics. // Make sure that the result of overload resolution is valid. var gotError = MemberGroupFinalValidationAccessibilityChecks(receiver, property, syntax, diagnostics, invokedAsExtensionMethod: false); @@ -10679,7 +10691,7 @@ candidate is PropertySymbol property && { Debug.Assert(!argIsIndex); // Look for Substring - var substring = (MethodSymbol)GetSpecialTypeMember(SpecialMember.System_String__Substring, diagnostics, syntax); + var substring = (MethodSymbol)GetSpecialTypeMember(SpecialMember.System_String__SubstringIntInt, diagnostics, syntax); if (substring is object) { makeCall(syntax, receiver, substring, out indexerOrSliceAccess, out argumentPlaceholders); diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs index f5a5848eeb4f..c18a721ad61d 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Invocation.cs @@ -1324,7 +1324,7 @@ private BoundCall BindInvocationExpressionContinued( { // Don't worry about double reporting (i.e. for both the argument and the parameter) // because only one unsafe diagnostic is allowed per scope - the others are suppressed. - gotError = ReportUnsafeIfNotAllowed(node, diagnostics) || gotError; + gotError = ReportUnsafeIfNotAllowed(node, diagnostics, disallowedUnder: MemorySafetyRules.Legacy) || gotError; } bool hasBaseReceiver = receiver != null && receiver.Kind == BoundKind.BaseReference; @@ -1332,6 +1332,7 @@ private BoundCall BindInvocationExpressionContinued( ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); ReportDiagnosticsIfDisallowedExtension(diagnostics, method, node); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, method, node); // No use site errors, but there could be use site warnings. // If there are any use site warnings, they have already been reported by overload resolution. @@ -2402,7 +2403,7 @@ private BoundExpression BindNameofOperatorInternal(InvocationExpressionSyntax no methodGroup.LookupError, methodGroup.Flags, methodGroup.FunctionType, - receiverOpt: ReplaceTypeOrValueReceiver(methodGroup.ReceiverOpt, useType: false, BindingDiagnosticBag.Discarded), //only change + receiverOpt: ReplaceTypeOrValueReceiver(methodGroup.ReceiverOpt, useType: true, boundArgument.HasAnyErrors ? BindingDiagnosticBag.Discarded : diagnostics), //only change methodGroup.ResultKind); } else if (boundArgument is BoundPropertyAccess propertyAccess) @@ -2581,7 +2582,8 @@ private BoundFunctionPointerInvocation BindFunctionPointerInvocation(SyntaxNode var args = analyzedArguments.Arguments.ToImmutable(); var refKinds = analyzedArguments.RefKinds.ToImmutableOrNull(); - bool hasErrors = ReportUnsafeIfNotAllowed(node, diagnostics); + bool hasErrors = ReportUnsafeIfNotAllowed(node, diagnostics, disallowedUnder: MemorySafetyRules.Legacy) || + ReportUnsafeIfNotAllowed(node, diagnostics, disallowedUnder: MemorySafetyRules.Updated); return new BoundFunctionPointerInvocation( node, boundExpression, diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs index 86f7056023dc..3f2e57b6d5ae 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Lookup.cs @@ -199,15 +199,14 @@ private void LookupAllExtensionMembersInSingleBinder(LookupResult result, string internal void EnumerateAllExtensionMembersInSingleBinder(ArrayBuilder result, string? name, int arity, LookupOptions options, Binder originalBinder, ref CompoundUseSiteInfo useSiteInfo, ref CompoundUseSiteInfo classicExtensionUseSiteInfo) { - PooledHashSet? implementationsToShadow = null; - - // 1. Collect new extension members var extensionCandidates = ArrayBuilder.GetInstance(); - this.GetCandidateExtensionMembersInSingleBinder(extensionCandidates, name, alternativeName: null, arity, options, originalBinder); + this.GetAllExtensionCandidatesInSingleBinder(extensionCandidates, name, alternativeName: null, arity, options, originalBinder); foreach (var candidate in extensionCandidates) { - SingleLookupResult resultOfThisMember = originalBinder.CheckViability(candidate, arity, options, null, diagnose: true, useSiteInfo: ref useSiteInfo); + bool isExtensionMethod = candidate is MethodSymbol { IsExtensionMethod: true }; + ref var useSiteInfoToUse = ref isExtensionMethod ? ref classicExtensionUseSiteInfo : ref useSiteInfo; + SingleLookupResult resultOfThisMember = originalBinder.CheckViability(candidate, arity, options, null, diagnose: true, useSiteInfo: ref useSiteInfoToUse); if (resultOfThisMember.Kind == LookupResultKind.Empty) { continue; @@ -215,36 +214,9 @@ internal void EnumerateAllExtensionMembersInSingleBinder(ArrayBuilder.GetInstance(); - implementationsToShadow.Add(toShadow); - } } extensionCandidates.Free(); - - // 2. Collect classic extension methods - var extensionMethods = ArrayBuilder.GetInstance(); - this.GetCandidateExtensionMethodsInSingleBinder(extensionMethods, name, arity, options, originalBinder: originalBinder); - - foreach (var method in extensionMethods) - { - // Prefer instance extension declarations vs. their implementations - if (implementationsToShadow is null || !implementationsToShadow.Remove(method.OriginalDefinition)) - { - SingleLookupResult resultOfThisMember = originalBinder.CheckViability(method, arity, options, null, diagnose: true, useSiteInfo: ref classicExtensionUseSiteInfo); - if (resultOfThisMember.Kind != LookupResultKind.Empty) - { - result.Add(resultOfThisMember); - } - } - } - - extensionMethods.Free(); - implementationsToShadow?.Free(); } #nullable disable @@ -814,15 +786,15 @@ internal virtual void GetCandidateExtensionMethodsInSingleBinder( #nullable enable /// - /// Return the extension members from this specific binding scope + /// Return the extension members and methods from this specific binding scope /// Since the lookup of extension members is iterative, proceeding one binding scope at a time, - /// should not defer to the next binding scope. Instead, the caller is + /// should not defer to the next binding scope. Instead, the caller is /// responsible for walking the nested binding scopes from innermost to outermost. This method is overridden /// to search the available members list in binding types that represent types, namespaces, and usings. /// An alternativeName should only be provided if a name is provided. /// /// Does not perform a full viability check - internal virtual void GetCandidateExtensionMembersInSingleBinder( + internal virtual void GetAllExtensionCandidatesInSingleBinder( ArrayBuilder members, string? name, string? alternativeName, diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs index 1b1ddf07bced..be42b5af9c0f 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Operators.cs @@ -198,11 +198,7 @@ BoundExpression bindCompoundAssignment(AssignmentExpressionSyntax node, ref Oper return createBadCompoundAssignmentOperator(node, kind, left, right, resultKind, originalUserDefinedOperators, ref operatorResolutionForReporting, diagnostics); } - if (best.Signature.Method is { } bestMethod) - { - ReportObsoleteAndFeatureAvailabilityDiagnostics(bestMethod, node, diagnostics); - ReportUseSite(bestMethod, diagnostics, node); - } + ReportOperatorUseSiteDiagnostics(best.Signature.Method, node, diagnostics); // The rules in the spec for determining additional errors are bit confusing. In particular // this line is misleading: @@ -480,6 +476,7 @@ bool shouldTryUserDefinedInstanceOperator(AssignmentExpressionSyntax node, bool BoundExpression rightConverted = CreateConversion(right, overloadResolutionResult.ValidResult.Result.ConversionForArg(isExtension ? 1 : 0), method.Parameters[0].Type, diagnostics); ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, method, node); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); BoundValuePlaceholder? leftPlaceholder = null; @@ -604,7 +601,7 @@ TypeSymbol getResultType(ExpressionSyntax node, TypeSymbol leftType, BindingDiag Debug.Assert(ordinaryInstanceOperatorName is not null); extensionCandidatesInSingleScope.Clear(); - scope.Binder.GetCandidateExtensionMembersInSingleBinder(extensionCandidatesInSingleScope, + scope.Binder.GetAllExtensionCandidatesInSingleBinder(extensionCandidatesInSingleScope, ordinaryInstanceOperatorName, checkedInstanceOperatorName, arity: 0, LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeOperator | LookupOptions.MustBeInstance, this); @@ -619,7 +616,7 @@ TypeSymbol getResultType(ExpressionSyntax node, TypeSymbol leftType, BindingDiag } extensionCandidatesInSingleScope.Clear(); - scope.Binder.GetCandidateExtensionMembersInSingleBinder(extensionCandidatesInSingleScope, + scope.Binder.GetAllExtensionCandidatesInSingleBinder(extensionCandidatesInSingleScope, staticOperatorName1, staticOperatorName2Opt, arity: 0, LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeOperator | LookupOptions.MustNotBeInstance, this); @@ -747,6 +744,7 @@ private BoundExpression BindEventAssignment(AssignmentExpressionSyntax node, Bou else { CheckReceiverAndRuntimeSupportForSymbolAccess(node, receiverOpt, method, diagnostics); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, method, node.OperatorToken); } if (eventSymbol.IsWindowsRuntimeEvent) @@ -1183,11 +1181,7 @@ bool bindSimpleBinaryOperatorPartsContinue( bool foundOperator; var signature = best.Signature; - if (signature.Method is { } bestMethod) - { - ReportObsoleteAndFeatureAvailabilityDiagnostics(bestMethod, node, diagnostics); - ReportUseSite(bestMethod, diagnostics, node); - } + ReportOperatorUseSiteDiagnostics(signature.Method, node, diagnostics); bool isObjectEquality = signature.Kind == BinaryOperatorKind.ObjectEqual || signature.Kind == BinaryOperatorKind.ObjectNotEqual; @@ -1506,11 +1500,7 @@ BoundExpression bindConditionalLogicalOperator(BinaryExpressionSyntax node, Boun // bool, or we've got a valid user-defined operator. BinaryOperatorSignature signature = best.Signature; - if (signature.Method is { } bestMethod) - { - ReportObsoleteAndFeatureAvailabilityDiagnostics(bestMethod, node, diagnostics); - ReportUseSite(bestMethod, diagnostics, node); - } + ReportOperatorUseSiteDiagnostics(signature.Method, node, diagnostics); bool bothBool = signature.LeftType.SpecialType == SpecialType.System_Boolean && signature.RightType.SpecialType == SpecialType.System_Boolean; @@ -1996,7 +1986,7 @@ bool isValidExtensionUserDefinedConditionalLogicalOperator( UnaryOperatorAnalysisResult? possiblyBest = null; string name = OperatorFacts.UnaryOperatorNameFromOperatorKind(kind, isChecked: false); - extensionContainingType.GetExtensionMembers(extensionCandidates, + extensionContainingType.GetAllExtensionMembers(extensionCandidates, name, alternativeName: null, arity: 0, LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeOperator | LookupOptions.MustNotBeInstance, FieldsBeingBound); @@ -2121,7 +2111,7 @@ private BinaryOperatorAnalysisResult BinaryOperatorOverloadResolution( foreach (var scope in new ExtensionScopes(this)) { extensionCandidatesInSingleScope.Clear(); - scope.Binder.GetCandidateExtensionMembersInSingleBinder(extensionCandidatesInSingleScope, + scope.Binder.GetAllExtensionCandidatesInSingleBinder(extensionCandidatesInSingleScope, name1, name2Opt, arity: 0, LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeOperator | LookupOptions.MustNotBeInstance, this); @@ -2222,11 +2212,13 @@ private static BinaryOperatorAnalysisResult BinaryOperatorAnalyzeOverloadResolut return possiblyBest; } - private void ReportObsoleteAndFeatureAvailabilityDiagnostics(MethodSymbol operatorMethod, SyntaxNode node, BindingDiagnosticBag diagnostics) + private void ReportOperatorUseSiteDiagnostics(MethodSymbol operatorMethod, SyntaxNode node, BindingDiagnosticBag diagnostics) { if ((object)operatorMethod != null) { + ReportUseSite(operatorMethod, diagnostics, node); ReportDiagnosticsIfObsolete(diagnostics, operatorMethod, node, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, operatorMethod, node); if (operatorMethod.ContainingType.IsInterface && operatorMethod.ContainingModule != Compilation.SourceModule) @@ -2389,8 +2381,7 @@ UnaryOperatorAnalysisResult AnalyzeUnaryOperatorOverloadResolutionResult( if (possiblyBest is { HasValue: true, Signature: { Method: { } bestMethod } }) { - ReportObsoleteAndFeatureAvailabilityDiagnostics(bestMethod, node, diagnostics); - ReportUseSite(bestMethod, diagnostics, node); + ReportOperatorUseSiteDiagnostics(bestMethod, node, diagnostics); } return possiblyBest; @@ -2433,7 +2424,7 @@ static bool isNuint(TypeSymbol type) foreach (var scope in new ExtensionScopes(this)) { extensionCandidatesInSingleScope.Clear(); - scope.Binder.GetCandidateExtensionMembersInSingleBinder(extensionCandidatesInSingleScope, + scope.Binder.GetAllExtensionCandidatesInSingleBinder(extensionCandidatesInSingleScope, name1, name2Opt, arity: 0, LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeOperator | LookupOptions.MustNotBeInstance, this); @@ -3568,6 +3559,7 @@ InstanceUserDefinedIncrementUsageMode getInstanceUserDefinedIncrementUsageMode( var method = overloadResolutionResult.ValidResult.Member; ReportDiagnosticsIfObsolete(diagnostics, method, node, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, method, node); ReportDiagnosticsIfUnmanagedCallersOnly(diagnostics, method, node, isDelegateConversion: false); BoundValuePlaceholder? operandPlaceholder = null; @@ -3695,7 +3687,7 @@ TypeSymbol getResultType(ExpressionSyntax node, TypeSymbol operandType, Instance Debug.Assert(ordinaryInstanceOperatorName is not null); extensionCandidatesInSingleScope.Clear(); - scope.Binder.GetCandidateExtensionMembersInSingleBinder(extensionCandidatesInSingleScope, + scope.Binder.GetAllExtensionCandidatesInSingleBinder(extensionCandidatesInSingleScope, ordinaryInstanceOperatorName, checkedInstanceOperatorName, arity: 0, LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeOperator | LookupOptions.MustBeInstance, this); @@ -3710,7 +3702,7 @@ TypeSymbol getResultType(ExpressionSyntax node, TypeSymbol operandType, Instance } extensionCandidatesInSingleScope.Clear(); - scope.Binder.GetCandidateExtensionMembersInSingleBinder(extensionCandidatesInSingleScope, + scope.Binder.GetAllExtensionCandidatesInSingleBinder(extensionCandidatesInSingleScope, staticOperatorName1, staticOperatorName2Opt, arity: 0, LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeOperator | LookupOptions.MustNotBeInstance, this); @@ -4067,7 +4059,14 @@ private BoundExpression BindPointerIndirectionExpression(PrefixUnaryExpressionSy bool hasErrors; BindPointerIndirectionExpressionInternal(node, operand, diagnostics, out pointedAtType, out hasErrors); - return new BoundPointerIndirectionOperator(node, operand, refersToLocation: false, pointedAtType ?? CreateErrorType(), hasErrors); + var result = new BoundPointerIndirectionOperator(node, operand, refersToLocation: false, pointedAtType ?? CreateErrorType(), hasErrors); + + if (!result.HasErrors) + { + ReportUnsafeIfNotAllowed(node.OperatorToken.GetLocation(), diagnostics, MemorySafetyRules.Updated); + } + + return result; } private static void BindPointerIndirectionExpressionInternal(CSharpSyntaxNode node, BoundExpression operand, BindingDiagnosticBag diagnostics, out TypeSymbol pointedAtType, out bool hasErrors) @@ -4824,6 +4823,10 @@ private BoundExpression BindIsOperator(BinaryExpressionSyntax node, BindingDiagn var operand = BindRValueWithoutTargetType(node.Left, diagnostics); var operandHasErrors = IsOperandErrors(node, ref operand, diagnostics); + TypeSymbol inputType = operand.Type; + NamedTypeSymbol unionMatchingInputType; + NamedTypeSymbol unionType = null; + // try binding as a type, but back off to binding as an expression if that does not work. bool wasUnderscore = IsUnderscore(node.Right); if (!tryBindAsType(node.Right, diagnostics, out BindingDiagnosticBag isTypeDiagnostics, out BoundTypeExpression typeExpression) && @@ -4832,7 +4835,7 @@ private BoundExpression BindIsOperator(BinaryExpressionSyntax node, BindingDiagn { // it did not bind as a type; try binding as a constant expression pattern var isPatternDiagnostics = BindingDiagnosticBag.GetInstance(diagnostics); - if ((object)operand.Type == null) + if ((object)inputType == null) { if (!operandHasErrors) { @@ -4840,29 +4843,75 @@ private BoundExpression BindIsOperator(BinaryExpressionSyntax node, BindingDiagn } operand = ToBadExpression(operand); + inputType = operand.Type; + } + + if (inputType is not null) + { + unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, isPatternDiagnostics); + } + else + { + unionMatchingInputType = null; } bool hasErrors = node.Right.HasErrors; - var convertedExpression = BindExpressionForPattern(operand.Type, node.Right, ref hasErrors, isPatternDiagnostics, out var constantValueOpt, out var wasExpression, out _); + var convertedExpression = BindExpressionForPattern(unionType, inputType, node.Right, ref hasErrors, isPatternDiagnostics, out var constantValueOpt, out var wasExpression, patternExpressionConversion: out _, out BoundExpression originalExpression); if (wasExpression) { hasErrors |= constantValueOpt is null; isTypeDiagnostics.Free(); diagnostics.AddRangeAndFree(isPatternDiagnostics); - var boundConstantPattern = new BoundConstantPattern( - node.Right, convertedExpression, constantValueOpt ?? ConstantValue.Bad, operand.Type, convertedExpression.Type ?? operand.Type, hasErrors) -#pragma warning disable format - { WasCompilerGenerated = true }; -#pragma warning restore format - return MakeIsPatternExpression(node, operand, boundConstantPattern, resultType, operandHasErrors, diagnostics); + + BoundConstantPattern boundConstantPattern; + + if (IsClassOrNullableValueTypeUnionNullPatternMatching(unionMatchingInputType, constantValueOpt)) + { + // Special case of a null test for a class Union or for a Nullable. + // For class its meaning is equivalent to: ( is null or .Value is null) + // For Nullable its meaning is equivalent to: ( is null or .GetValueOrDefault().Value is null) + // Therefore, the type isn't narrowed by this pattern and the following pattern, if any, will do union matching from scratch. + + // Ensure that the null value can actually be also matched against the original input type, since we are matching it against the input value as well. + BindExpressionForPatternContinued(originalExpression, unionType: null, inputType: unionMatchingInputType, patternExpression: node.Right, ref hasErrors, diagnostics, constantValueOpt: out _, patternExpressionConversion: out _); + + boundConstantPattern = new BoundConstantPattern( + node.Right, convertedExpression, constantValueOpt, isUnionMatching: true, inputType: unionMatchingInputType, narrowedType: unionMatchingInputType, hasErrors).MakeCompilerGenerated(); + } + else + { + + boundConstantPattern = new BoundConstantPattern( + node.Right, convertedExpression, constantValueOpt ?? ConstantValue.Bad, isUnionMatching: unionMatchingInputType is not null, inputType: unionMatchingInputType ?? inputType, convertedExpression.Type ?? inputType, hasErrors).MakeCompilerGenerated(); + } + + return MakeIsPatternExpression(node, operand, boundConstantPattern, boundConstantPattern.IsUnionMatching, resultType, operandHasErrors, diagnostics); } isPatternDiagnostics.Free(); } diagnostics.AddRangeAndFree(isTypeDiagnostics); - var targetTypeWithAnnotations = typeExpression.TypeWithAnnotations; var targetType = typeExpression.Type; + + if (inputType is not null) + { + unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + } + else + { + unionMatchingInputType = null; + } + + if (unionMatchingInputType is not null) + { + bool hasErrors = CheckValidPatternType(node.Right, unionType, inputType, targetType, diagnostics: diagnostics); + // https://github.com/dotnet/roslyn/issues/82636: Add test coverage for isExplicitNotNullTest + var pattern = new BoundTypePattern(node, typeExpression, isExplicitNotNullTest: false, isUnionMatching: true, inputType: unionMatchingInputType, targetType, hasErrors); + return MakeIsPatternExpression(node, operand, pattern.MakeCompilerGenerated(), hasUnionMatching: true, resultType, operandHasErrors, diagnostics); + } + + var targetTypeWithAnnotations = typeExpression.TypeWithAnnotations; if (targetType.IsReferenceType && targetTypeWithAnnotations.NullableAnnotation.IsAnnotated()) { Error(diagnostics, ErrorCode.ERR_IsNullableType, node.Right, targetType); @@ -4870,7 +4919,7 @@ private BoundExpression BindIsOperator(BinaryExpressionSyntax node, BindingDiagn } var targetTypeKind = targetType.TypeKind; - if (operandHasErrors || IsOperatorErrors(node, operand.Type, typeExpression, diagnostics)) + if (operandHasErrors || IsOperatorErrors(node, inputType, typeExpression, diagnostics)) { return new BoundIsOperator(node, operand, typeExpression, ConversionKind.NoConversion, resultType, hasErrors: true); } @@ -4891,7 +4940,7 @@ private BoundExpression BindIsOperator(BinaryExpressionSyntax node, BindingDiagn if (operand.ConstantValueOpt == ConstantValue.Null || operand.Kind == BoundKind.MethodGroup || - operand.Type.IsVoidType()) + inputType.IsVoidType()) { // warning for cases where the result is always false: // (a) "null is TYPE" OR operand evaluates to null @@ -4921,17 +4970,16 @@ private BoundExpression BindIsOperator(BinaryExpressionSyntax node, BindingDiagn ); } - var operandType = operand.Type; - Debug.Assert((object)operandType != null); - if (operandType.TypeKind == TypeKind.Dynamic) + Debug.Assert((object)inputType != null); + if (inputType.TypeKind == TypeKind.Dynamic) { // if operand has a dynamic type, we do the same thing as though it were an object - operandType = GetSpecialType(SpecialType.System_Object, diagnostics, node); + inputType = GetSpecialType(SpecialType.System_Object, diagnostics, node); } - Conversion conversion = Conversions.ClassifyBuiltInConversion(operandType, targetType, isChecked: CheckOverflowAtRuntime, ref useSiteInfo); + Conversion conversion = Conversions.ClassifyBuiltInConversion(inputType, targetType, isChecked: CheckOverflowAtRuntime, ref useSiteInfo); diagnostics.Add(node, useSiteInfo); - ReportIsOperatorDiagnostics(node, diagnostics, operandType, targetType, conversion.Kind, operand.ConstantValueOpt); + ReportIsOperatorDiagnostics(node, diagnostics, inputType, targetType, conversion.Kind, operand.ConstantValueOpt); return new BoundIsOperator(node, operand, typeExpression, conversion.Kind, resultType); bool tryBindAsType( @@ -5284,6 +5332,7 @@ internal static ConstantValue GetIsOperatorConstantResult( case ConversionKind.NullLiteral: case ConversionKind.DefaultLiteral: case ConversionKind.MethodGroup: + case ConversionKind.Union: // We've either replaced Dynamic with Object, or already bailed out with an error. throw ExceptionUtilities.UnexpectedValue(conversionKind); } diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Patterns.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Patterns.cs index 777c0fbe5262..7461720b0f4c 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Patterns.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Patterns.cs @@ -16,6 +16,190 @@ namespace Microsoft.CodeAnalysis.CSharp { partial class Binder { + protected NamedTypeSymbol? PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(SyntaxNode node, ref TypeSymbol inputType, ref NamedTypeSymbol? unionType, BindingDiagnosticBag diagnostics) + { + if (inputType.IsUnionMatchingInputType(out var unionTypeOverride)) + { + MessageID.IDS_FeatureUnions.CheckFeatureAvailability(diagnostics, node); + + var originalInputType = (NamedTypeSymbol)inputType; + CompoundUseSiteInfo useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics); + inputType = GetUnionTypeValueProperty(unionTypeOverride, ref useSiteInfo)?.Type ?? Compilation.GetSpecialType(SpecialType.System_Object); + diagnostics.Add(node, useSiteInfo); + unionType = unionTypeOverride; + return originalInputType; + } + + return null; + } + + internal static PropertySymbol? GetUnionTypeValueProperty(NamedTypeSymbol inputUnionType, ref CompoundUseSiteInfo useSiteInfo) + { + PropertySymbol? valueProperty = GetUnionTypeValuePropertyOriginalDefinition(inputUnionType, ref useSiteInfo); + if (valueProperty is null) + { + return null; + } + + return valueProperty.AsMember(inputUnionType); + } + + private static PropertySymbol? GetUnionTypeValuePropertyOriginalDefinition(NamedTypeSymbol inputUnionType, ref CompoundUseSiteInfo useSiteInfo) + { + Debug.Assert(inputUnionType.IsUnionType); + + // https://github.com/dotnet/roslyn/issues/82636: Not dealing with inheritance for now. + // The inherited property might not be a definition, therefore the name of the function should probably change + PropertySymbol? valueProperty = null; + foreach (var m in inputUnionType.OriginalDefinition.GetMembers(WellKnownMemberNames.ValuePropertyName)) + { + if (m is PropertySymbol prop && hasUnionValueSignature(prop)) + { + valueProperty = prop; + break; + } + } + + if (valueProperty is not null) + { + Debug.Assert(valueProperty.Type.IsObjectType()); + useSiteInfo.Add(valueProperty.GetUseSiteInfo()); + } + else + { + useSiteInfo.AddDiagnosticInfo(new CSDiagnosticInfo(ErrorCode.ERR_MissingPredefinedMember, inputUnionType, WellKnownMemberNames.ValuePropertyName)); // https://github.com/dotnet/roslyn/issues/82636: Cover this code path + } + + return valueProperty; + + static bool hasUnionValueSignature(PropertySymbol property) + { + // https://github.com/dotnet/roslyn/issues/82636: Cover individual conditions with tests + // https://github.com/dotnet/roslyn/issues/82636: Cover scenaros with a setter present + return property is + { + IsStatic: false, + DeclaredAccessibility: Accessibility.Public, + GetMethod: not null, + RefKind: RefKind.None, + ParameterCount: 0, + Type.SpecialType: SpecialType.System_Object + }; + } + } + + internal static PropertySymbol? GetUnionTypeValuePropertyNoUseSiteDiagnostics(NamedTypeSymbol inputUnionType) + { + var useSiteInfo = CompoundUseSiteInfo.Discarded; + return GetUnionTypeValueProperty(inputUnionType, ref useSiteInfo); + } + + internal static bool IsUnionTypeValueProperty(NamedTypeSymbol unionType, Symbol symbol) + { + // https://github.com/dotnet/roslyn/issues/82636: Deal with inheritance? + var useSiteInfo = CompoundUseSiteInfo.Discarded; + return Binder.GetUnionTypeValuePropertyOriginalDefinition(unionType, ref useSiteInfo) == (object)symbol.OriginalDefinition; + } + + private static PropertySymbol? GetUnionTypeHasValuePropertyOriginalDefinition(NamedTypeSymbol inputUnionType) + { + Debug.Assert(inputUnionType.IsUnionType); + + PropertySymbol? valueProperty = null; + + // https://github.com/dotnet/roslyn/issues/82636: Not dealing with inheritance for now. + foreach (var m in inputUnionType.OriginalDefinition.GetMembers(WellKnownMemberNames.HasValuePropertyName)) + { + if (m is PropertySymbol prop && hasHasValueSignature(prop)) + { + valueProperty = prop; + break; + } + } + + if (valueProperty is null || valueProperty.GetUseSiteInfo().DiagnosticInfo?.DefaultSeverity == DiagnosticSeverity.Error) + { + return null; // https://github.com/dotnet/roslyn/issues/82636: Cover this code path + } + + return valueProperty; + + static bool hasHasValueSignature(PropertySymbol property) + { + // https://github.com/dotnet/roslyn/issues/82636: Cover individual conditions with tests + return property is + { + IsStatic: false, + DeclaredAccessibility: Accessibility.Public, + GetMethod: not null, + SetMethod: null, + RefKind: RefKind.None, + ParameterCount: 0, + Type.SpecialType: SpecialType.System_Boolean + }; + } + } + + internal static PropertySymbol? GetUnionTypeHasValueProperty(NamedTypeSymbol inputUnionType) + { + PropertySymbol? valueProperty = GetUnionTypeHasValuePropertyOriginalDefinition(inputUnionType); + if (valueProperty is null) + { + return null; + } + + return valueProperty.AsMember(inputUnionType); + } + + internal static MethodSymbol? GetUnionTypeTryGetValueMethod(NamedTypeSymbol inputUnionType, TypeSymbol type) + { + Debug.Assert(inputUnionType.IsUnionType); + + // https://github.com/dotnet/roslyn/issues/82636: Not dealing with inheritance for now. + NamedTypeSymbol unionDefinition = inputUnionType.OriginalDefinition; + ImmutableArray caseTypes = unionDefinition.UnionCaseTypes; + + foreach (var m in unionDefinition.GetMembers(WellKnownMemberNames.TryGetValueMethodName)) + { + if (m is MethodSymbol method && isTryGetValueSignature(method)) + { + var candidate = method.AsMember(inputUnionType); + if (candidate.Parameters[0].Type.Equals(type, TypeCompareKind.AllIgnoreOptions) && // https://github.com/dotnet/roslyn/issues/82636: Allow conversions per spec + caseTypes.Contains(method.Parameters[0].Type, Symbols.SymbolEqualityComparer.AllIgnoreOptions)) // https://github.com/dotnet/roslyn/issues/82636: Optimize this check? + { + if (method.GetUseSiteInfo().DiagnosticInfo?.DefaultSeverity == DiagnosticSeverity.Error) + { + continue; // https://github.com/dotnet/roslyn/issues/82636: Cover this code path + } + + return candidate; + } + } + } + + return null; + + static bool isTryGetValueSignature(MethodSymbol method) + { + // https://github.com/dotnet/roslyn/issues/82636: Cover individual conditions with tests + return method is + { + IsStatic: false, + DeclaredAccessibility: Accessibility.Public, + Arity: 0, + RefKind: RefKind.None, + Parameters: [{ RefKind: RefKind.Out }], + ReturnType.SpecialType: SpecialType.System_Boolean + }; + } + } + + internal static bool IsUnionTypeHasValueProperty(NamedTypeSymbol unionType, PropertySymbol property) + { + // https://github.com/dotnet/roslyn/issues/82636: Deal with inheritance? + return (object)property.OriginalDefinition == Binder.GetUnionTypeHasValuePropertyOriginalDefinition(unionType); + } + private BoundExpression BindIsPatternExpression(IsPatternExpressionSyntax node, BindingDiagnosticBag diagnostics) { MessageID.IDS_FeaturePatternMatching.CheckFeatureAvailability(diagnostics, node.IsKeyword); @@ -36,10 +220,11 @@ private BoundExpression BindIsPatternExpression(IsPatternExpressionSyntax node, } Debug.Assert(expression.Type is { }); - BoundPattern pattern = BindPattern(node.Pattern, expression.Type, permitDesignations: true, hasErrors, diagnostics, underIsPattern: true); + NamedTypeSymbol? unionType = null; + BoundPattern pattern = BindPattern(node.Pattern, ref unionType, expression.Type, permitDesignations: true, hasErrors, diagnostics, out bool hasUnionMatching, underIsPattern: true); hasErrors |= pattern.HasErrors; return MakeIsPatternExpression( - node, expression, pattern, GetSpecialType(SpecialType.System_Boolean, diagnostics, node), + node, expression, pattern, hasUnionMatching, GetSpecialType(SpecialType.System_Boolean, diagnostics, node), hasErrors, diagnostics); } @@ -47,6 +232,7 @@ private BoundExpression MakeIsPatternExpression( SyntaxNode node, BoundExpression expression, BoundPattern pattern, + bool hasUnionMatching, TypeSymbol boolType, bool hasErrors, BindingDiagnosticBag diagnostics) @@ -57,7 +243,7 @@ private BoundExpression MakeIsPatternExpression( bool negated = pattern.IsNegated(out var innerPattern); BoundDecisionDag decisionDag = DecisionDagBuilder.CreateDecisionDagForIsPattern( - this.Compilation, pattern.Syntax, expression, innerPattern, whenTrueLabel: whenTrueLabel, whenFalseLabel: whenFalseLabel, diagnostics); + this.Compilation, pattern.Syntax, expression, innerPattern, hasUnionMatching, whenTrueLabel: whenTrueLabel, whenFalseLabel: whenFalseLabel, diagnostics); bool wasReported = false; if (!hasErrors && getConstantResult(decisionDag, negated, whenTrueLabel, whenFalseLabel) is { } constantResult) @@ -130,13 +316,13 @@ private BoundExpression MakeIsPatternExpression( if (!wasReported && diagnostics.AccumulatesDiagnostics && DecisionDagBuilder.EnableRedundantPatternsCheck(this.Compilation)) { - DecisionDagBuilder.CheckRedundantPatternsForIsPattern(this.Compilation, pattern.Syntax, expression, pattern, diagnostics); + DecisionDagBuilder.CheckRedundantPatternsForIsPattern(this.Compilation, pattern.Syntax, expression, pattern, hasUnionMatching, diagnostics); } // decisionDag, whenTrueLabel, and whenFalseLabel represent the decision DAG for the inner pattern, // after removing any outer 'not's, so consumers will need to compensate for negated patterns. return new BoundIsPatternExpression( - node, expression, pattern, negated, decisionDag, whenTrueLabel: whenTrueLabel, whenFalseLabel: whenFalseLabel, boolType, hasErrors); + node, expression, pattern, hasUnionMatching, negated, decisionDag, whenTrueLabel: whenTrueLabel, whenFalseLabel: whenFalseLabel, boolType, hasErrors); static bool? getConstantResult(BoundDecisionDag decisionDag, bool negated, LabelSymbol whenTrueLabel, LabelSymbol whenFalseLabel) { @@ -174,40 +360,45 @@ internal virtual BoundExpression BindSwitchExpressionCore( internal BoundPattern BindPattern( PatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool permitDesignations, bool hasErrors, BindingDiagnosticBag diagnostics, + out bool hasUnionMatching, bool underIsPattern = false) { + hasUnionMatching = false; return node switch { DiscardPatternSyntax p => BindDiscardPattern(p, inputType, diagnostics), - DeclarationPatternSyntax p => BindDeclarationPattern(p, inputType, permitDesignations, hasErrors, diagnostics), - ConstantPatternSyntax p => BindConstantPatternWithFallbackToTypePattern(p, inputType, hasErrors, diagnostics), - RecursivePatternSyntax p => BindRecursivePattern(p, inputType, permitDesignations, hasErrors, diagnostics), - VarPatternSyntax p => BindVarPattern(p, inputType, permitDesignations, hasErrors, diagnostics), - ParenthesizedPatternSyntax p => BindParenthesizedPattern(p, inputType, permitDesignations, hasErrors, diagnostics, underIsPattern), - BinaryPatternSyntax p => BindBinaryPattern(p, inputType, permitDesignations, hasErrors, diagnostics), - UnaryPatternSyntax p => BindUnaryPattern(p, inputType, hasErrors, diagnostics, underIsPattern), - RelationalPatternSyntax p => BindRelationalPattern(p, inputType, hasErrors, diagnostics), - TypePatternSyntax p => BindTypePattern(p, inputType, hasErrors, diagnostics), - ListPatternSyntax p => BindListPattern(p, inputType, permitDesignations, hasErrors, diagnostics), - SlicePatternSyntax p => BindSlicePattern(p, inputType, permitDesignations, ref hasErrors, misplaced: true, diagnostics), + DeclarationPatternSyntax p => BindDeclarationPattern(p, ref unionType, inputType, permitDesignations, hasErrors, diagnostics, hasUnionMatching: out hasUnionMatching), + ConstantPatternSyntax p => BindConstantPatternWithFallbackToTypePattern(p, ref unionType, inputType, hasErrors, diagnostics, hasUnionMatching: out hasUnionMatching), + RecursivePatternSyntax p => BindRecursivePattern(p, ref unionType, inputType, permitDesignations, hasErrors, diagnostics, hasUnionMatching: out hasUnionMatching), + VarPatternSyntax p => BindVarPattern(p, ref unionType, inputType, permitDesignations, hasErrors, diagnostics, hasUnionMatching: out hasUnionMatching), + ParenthesizedPatternSyntax p => BindParenthesizedPattern(p, ref unionType, inputType, permitDesignations, hasErrors, diagnostics, underIsPattern, hasUnionMatching: out hasUnionMatching), + BinaryPatternSyntax p => BindBinaryPattern(p, ref unionType, inputType, permitDesignations, hasErrors, diagnostics, hasUnionMatching: out hasUnionMatching), + UnaryPatternSyntax p => BindUnaryPattern(p, ref unionType, inputType, hasErrors, diagnostics, underIsPattern, hasUnionMatching: out hasUnionMatching), + RelationalPatternSyntax p => BindRelationalPattern(p, ref unionType, inputType, hasErrors, diagnostics, hasUnionMatching: out hasUnionMatching), + TypePatternSyntax p => BindTypePattern(p, ref unionType, inputType, hasErrors, diagnostics, hasUnionMatching: out hasUnionMatching), + ListPatternSyntax p => BindListPattern(p, ref unionType, inputType, permitDesignations, hasErrors, diagnostics, hasUnionMatching: out hasUnionMatching), + SlicePatternSyntax p => BindSlicePattern(p, inputType, permitDesignations, ref hasErrors, misplaced: true, diagnostics, hasUnionMatching: out hasUnionMatching), _ => throw ExceptionUtilities.UnexpectedValue(node.Kind()), }; } private BoundPattern BindParenthesizedPattern( ParenthesizedPatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool permitDesignations, bool hasErrors, BindingDiagnosticBag diagnostics, - bool underIsPattern) + bool underIsPattern, + out bool hasUnionMatching) { MessageID.IDS_FeatureParenthesizedPattern.CheckFeatureAvailability(diagnostics, node.OpenParenToken); - return BindPattern(node.Pattern, inputType, permitDesignations, hasErrors, diagnostics, underIsPattern); + return BindPattern(node.Pattern, ref unionType, inputType, permitDesignations, hasErrors, diagnostics, out hasUnionMatching, underIsPattern); } private BoundPattern BindSlicePattern( @@ -216,7 +407,8 @@ private BoundPattern BindSlicePattern( bool permitDesignations, ref bool hasErrors, bool misplaced, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { if (misplaced && !hasErrors) { @@ -261,7 +453,12 @@ private BoundPattern BindSlicePattern( sliceType = indexerAccess.Type; } - pattern = BindPattern(node.Pattern, sliceType, permitDesignations, hasErrors, diagnostics); + NamedTypeSymbol? unionType = null; + pattern = BindPattern(node.Pattern, ref unionType, sliceType, permitDesignations, hasErrors, diagnostics, out hasUnionMatching); + } + else + { + hasUnionMatching = false; } return new BoundSlicePattern(node, pattern, indexerAccess, receiverPlaceholder, argumentPlaceholder, inputType: inputType, narrowedType: inputType, hasErrors); @@ -274,21 +471,26 @@ private ImmutableArray BindListPatternSubpatterns( bool permitDesignations, ref bool hasErrors, out bool sawSlice, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { sawSlice = false; + hasUnionMatching = false; var builder = ArrayBuilder.GetInstance(subpatterns.Count); foreach (PatternSyntax pattern in subpatterns) { BoundPattern boundPattern; if (pattern is SlicePatternSyntax slice) { - boundPattern = BindSlicePattern(slice, inputType, permitDesignations, ref hasErrors, misplaced: sawSlice, diagnostics: diagnostics); + boundPattern = BindSlicePattern(slice, inputType, permitDesignations, ref hasErrors, misplaced: sawSlice, diagnostics: diagnostics, out bool sliceHasUnionMatching); + hasUnionMatching |= sliceHasUnionMatching; sawSlice = true; } else { - boundPattern = BindPattern(pattern, elementType, permitDesignations, hasErrors, diagnostics); + NamedTypeSymbol? unionType = null; + boundPattern = BindPattern(pattern, ref unionType, elementType, permitDesignations, hasErrors, diagnostics, out bool patternHasUnionMatching); + hasUnionMatching |= patternHasUnionMatching; } builder.Add(boundPattern); @@ -299,11 +501,16 @@ private ImmutableArray BindListPatternSubpatterns( private BoundListPattern BindListPattern( ListPatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool permitDesignations, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { + NamedTypeSymbol? unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + bool isUnionMatching = unionMatchingInputType is not null; + CheckFeatureAvailability(node, MessageID.IDS_FeatureListPattern, diagnostics); TypeSymbol elementType; @@ -337,7 +544,9 @@ private BoundListPattern BindListPattern( ImmutableArray subpatterns = BindListPatternSubpatterns( node.Patterns, inputType: narrowedType, elementType: elementType, - permitDesignations, ref hasErrors, out bool sawSlice, diagnostics); + permitDesignations, ref hasErrors, out bool sawSlice, diagnostics, out hasUnionMatching); + + hasUnionMatching |= isUnionMatching; BindPatternDesignation( node.Designation, @@ -348,7 +557,7 @@ private BoundListPattern BindListPattern( return new BoundListPattern( syntax: node, subpatterns: subpatterns, hasSlice: sawSlice, lengthAccess: lengthAccess, indexerAccess: indexerAccess, receiverPlaceholder, argumentPlaceholder, variable: variableSymbol, - variableAccess: variableAccess, inputType: inputType, narrowedType: narrowedType, hasErrors); + variableAccess: variableAccess, isUnionMatching: isUnionMatching, inputType: unionMatchingInputType ?? inputType, narrowedType: narrowedType, hasErrors); } /// @@ -418,22 +627,30 @@ private static BoundPattern BindDiscardPattern(DiscardPatternSyntax node, TypeSy private BoundPattern BindConstantPatternWithFallbackToTypePattern( ConstantPatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { - return BindConstantPatternWithFallbackToTypePattern(node, node.Expression, inputType, hasErrors, diagnostics); + return BindConstantPatternWithFallbackToTypePattern(node, node.Expression, ref unionType, inputType, hasErrors, diagnostics, out hasUnionMatching); } internal BoundPattern BindConstantPatternWithFallbackToTypePattern( SyntaxNode node, ExpressionSyntax expression, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { + NamedTypeSymbol? unionTypeOnEntry = unionType; + NamedTypeSymbol? unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + hasUnionMatching = unionMatchingInputType is not null; + ExpressionSyntax innerExpression = SkipParensAndNullSuppressions(expression, diagnostics, ref hasErrors); - var convertedExpression = BindExpressionOrTypeForPattern(inputType, innerExpression, ref hasErrors, diagnostics, out var constantValueOpt, out bool wasExpression, out Conversion patternConversion); + var convertedExpression = BindExpressionOrTypeForPattern(unionType, inputType, innerExpression, ref hasErrors, diagnostics, out var constantValueOpt, out bool wasExpression, out Conversion patternConversion, out BoundExpression originalExpression); if (wasExpression) { var convertedType = convertedExpression.Type ?? inputType; @@ -448,8 +665,24 @@ internal BoundPattern BindConstantPatternWithFallbackToTypePattern( diagnostics.Add(ErrorCode.ERR_CannotMatchOnINumberBase, node.Location, inputType); } + if (IsClassOrNullableValueTypeUnionNullPatternMatching(unionMatchingInputType, constantValueOpt)) + { + Debug.Assert(hasUnionMatching); + // Special case of a null test for a class Union or for a Nullable. + // For class its meaning is equivalent to: ( is null or .Value is null) + // For Nullable its meaning is equivalent to: ( is null or .GetValueOrDefault().Value is null) + // Therefore, the type isn't narrowed by this pattern and the following pattern, if any, will do union matching from scratch. + + // Ensure that the null value can actually be also matched against the original input type, since we are matching it against the input value as well. + BindExpressionForPatternContinued(originalExpression, unionType: null, inputType: unionMatchingInputType, patternExpression: innerExpression, ref hasErrors, diagnostics, constantValueOpt: out _, patternExpressionConversion: out _); + + unionType = unionTypeOnEntry; + return new BoundConstantPattern( + node, convertedExpression, constantValueOpt, isUnionMatching: true, inputType: unionMatchingInputType, narrowedType: unionMatchingInputType, hasErrors); + } + return new BoundConstantPattern( - node, convertedExpression, constantValueOpt ?? ConstantValue.Bad, inputType, convertedType, hasErrors || constantValueOpt is null); + node, convertedExpression, constantValueOpt ?? ConstantValue.Bad, isUnionMatching: hasUnionMatching, inputType: unionMatchingInputType ?? inputType, convertedType, hasErrors || constantValueOpt is null); } else { @@ -457,11 +690,17 @@ internal BoundPattern BindConstantPatternWithFallbackToTypePattern( CheckFeatureAvailability(innerExpression, MessageID.IDS_FeatureTypePattern, diagnostics); var boundType = (BoundTypeExpression)convertedExpression; - bool isExplicitNotNullTest = boundType.Type.SpecialType == SpecialType.System_Object; - return new BoundTypePattern(node, boundType, isExplicitNotNullTest, inputType, boundType.Type, hasErrors); + bool isExplicitNotNullTest = !hasUnionMatching && boundType.Type.SpecialType == SpecialType.System_Object; // https://github.com/dotnet/roslyn/issues/82636: Add test coverage + return new BoundTypePattern(node, boundType, isExplicitNotNullTest, isUnionMatching: hasUnionMatching, inputType: unionMatchingInputType ?? inputType, boundType.Type, hasErrors); } } + internal static bool IsClassOrNullableValueTypeUnionNullPatternMatching([NotNullWhen(true)] NamedTypeSymbol? unionMatchingInputType, [NotNullWhen(true)] ConstantValue? constantValueOpt) + { + Debug.Assert(unionMatchingInputType is null || unionMatchingInputType.IsSubjectForUnionMatching); + return unionMatchingInputType is not null && constantValueOpt == ConstantValue.Null && (unionMatchingInputType.IsNullableType() || !unionMatchingInputType.IsValueType); + } + private bool ShouldBlockINumberBaseConversion(Conversion patternConversion, TypeSymbol inputType) { // We want to block constant and relation patterns that have an input type constrained to or inherited from INumberBase, if we don't @@ -507,27 +746,29 @@ private static ExpressionSyntax SkipParensAndNullSuppressions(ExpressionSyntax e /// and in that case it returns a . /// private BoundExpression BindExpressionOrTypeForPattern( + NamedTypeSymbol? unionType, TypeSymbol inputType, ExpressionSyntax patternExpression, ref bool hasErrors, BindingDiagnosticBag diagnostics, out ConstantValue? constantValueOpt, out bool wasExpression, - out Conversion patternExpressionConversion) + out Conversion patternExpressionConversion, + out BoundExpression originalExpression) { constantValueOpt = null; - BoundExpression expression = BindTypeOrRValue(patternExpression, diagnostics); - wasExpression = expression.Kind != BoundKind.TypeExpression; + originalExpression = BindTypeOrRValue(patternExpression, diagnostics); + wasExpression = originalExpression.Kind != BoundKind.TypeExpression; if (wasExpression) { - return BindExpressionForPatternContinued(expression, inputType, patternExpression, ref hasErrors, diagnostics, out constantValueOpt, out patternExpressionConversion); + return BindExpressionForPatternContinued(originalExpression, unionType, inputType, patternExpression, ref hasErrors, diagnostics, out constantValueOpt, out patternExpressionConversion); } else { - Debug.Assert(expression is { Kind: BoundKind.TypeExpression, Type: { } }); - hasErrors |= CheckValidPatternType(patternExpression, inputType, expression.Type, diagnostics: diagnostics); + Debug.Assert(originalExpression is { Kind: BoundKind.TypeExpression, Type: { } }); + hasErrors |= CheckValidPatternType(patternExpression, unionType, inputType, originalExpression.Type, diagnostics: diagnostics); patternExpressionConversion = Conversion.NoConversion; - return expression; + return originalExpression; } } @@ -535,24 +776,27 @@ private BoundExpression BindExpressionOrTypeForPattern( /// Binds the expression for an is-type right-hand-side, in case it does not bind as a type. /// private BoundExpression BindExpressionForPattern( + NamedTypeSymbol? unionType, TypeSymbol inputType, ExpressionSyntax patternExpression, ref bool hasErrors, BindingDiagnosticBag diagnostics, out ConstantValue? constantValueOpt, out bool wasExpression, - out Conversion patternExpressionConversion) + out Conversion patternExpressionConversion, + out BoundExpression originalExpression) { constantValueOpt = null; - var expression = BindExpression(patternExpression, diagnostics: diagnostics, invoked: false, indexed: false); - expression = CheckValue(expression, BindValueKind.RValue, diagnostics); - wasExpression = expression.Kind switch { BoundKind.BadExpression => false, BoundKind.TypeExpression => false, _ => true }; + originalExpression = BindExpression(patternExpression, diagnostics: diagnostics, invoked: false, indexed: false); + originalExpression = CheckValue(originalExpression, BindValueKind.RValue, diagnostics); + wasExpression = originalExpression.Kind switch { BoundKind.BadExpression => false, BoundKind.TypeExpression => false, _ => true }; patternExpressionConversion = Conversion.NoConversion; - return wasExpression ? BindExpressionForPatternContinued(expression, inputType, patternExpression, ref hasErrors, diagnostics, out constantValueOpt, out patternExpressionConversion) : expression; + return wasExpression ? BindExpressionForPatternContinued(originalExpression, unionType, inputType, patternExpression, ref hasErrors, diagnostics, out constantValueOpt, out patternExpressionConversion) : originalExpression; } private BoundExpression BindExpressionForPatternContinued( BoundExpression expression, + NamedTypeSymbol? unionType, TypeSymbol inputType, ExpressionSyntax patternExpression, ref bool hasErrors, @@ -561,7 +805,7 @@ private BoundExpression BindExpressionForPatternContinued( out Conversion patternExpressionConversion) { BoundExpression convertedExpression = ConvertPatternExpression( - inputType, patternExpression, expression, out constantValueOpt, hasErrors, diagnostics, out patternExpressionConversion); + unionType, inputType, patternExpression, expression, out constantValueOpt, hasErrors, diagnostics, out patternExpressionConversion); ConstantValueUtils.CheckLangVersionForConstantValue(convertedExpression, diagnostics); @@ -597,6 +841,7 @@ private BoundExpression BindExpressionForPatternContinued( } internal BoundExpression ConvertPatternExpression( + NamedTypeSymbol? unionType, TypeSymbol inputType, CSharpSyntaxNode node, BoundExpression expression, @@ -611,6 +856,7 @@ internal BoundExpression ConvertPatternExpression( // This permits us to match a value of type `IComparable` with a pattern of type `int`. if (inputType.ContainsTypeParameter()) { + // https://github.com/dotnet/roslyn/issues/82636: Make sure code in this block makes sense for union matching. convertedExpression = expression; // If the expression does not have a constant value, an error will be reported in the caller if (!hasErrors && expression.ConstantValueOpt is object) @@ -724,6 +970,51 @@ internal BoundExpression ConvertPatternExpression( } constantValue = convertedExpression.ConstantValueOpt; + + if (!hasErrors && unionType is not null && !convertedExpression.HasErrors && constantValue is { IsBad: false } && expression.Type is not null) + { + var unionDiagnostics = BindingDiagnosticBag.GetInstance(withDiagnostics: true, withDependencies: diagnostics.AccumulatesDependencies); + bool matched = false; + foreach (var caseType in unionType.UnionCaseTypes) + { + var caseDiagnostics = BindingDiagnosticBag.GetInstance(unionDiagnostics); + ConvertPatternExpression( + unionType: null, + inputType: caseType, + node: node, + expression, + constantValue: out var caseConstant, + hasErrors: false, + caseDiagnostics, + patternExpressionConversion: out _); + + if (caseConstant is { IsBad: false } && !caseDiagnostics.HasAnyResolvedErrors()) + { + caseDiagnostics.Free(); + matched = true; + break; + } + else + { + if (!caseDiagnostics.HasAnyResolvedErrors()) + { + diagnostics.Add(ErrorCode.ERR_PatternWrongType, expression.Syntax.Location, caseType, expression.Display); + } + + unionDiagnostics.AddRangeAndFree(caseDiagnostics); + } + } + + if (!matched) + { + constantValue = ConstantValue.Bad; + diagnostics.Add(ErrorCode.ERR_UnionMatchingWrongPattern, expression.Syntax.Location, unionType); + diagnostics.AddRange(unionDiagnostics); + } + + unionDiagnostics.Free(); + } + return convertedExpression; } @@ -732,12 +1023,14 @@ internal BoundExpression ConvertPatternExpression( /// private bool CheckValidPatternType( SyntaxNode typeSyntax, + NamedTypeSymbol? unionType, TypeSymbol inputType, TypeSymbol patternType, BindingDiagnosticBag diagnostics) { - RoslynDebug.Assert((object)inputType != null); - RoslynDebug.Assert((object)patternType != null); + Debug.Assert((object)inputType != null); + Debug.Assert((object)patternType != null); + Debug.Assert(unionType is null || unionType.IsUnionType); if (inputType.IsErrorType() || patternType.IsErrorType()) { @@ -773,10 +1066,37 @@ private bool CheckValidPatternType( return true; } + ConstantValue matchPossible; + Conversion conversion; + CompoundUseSiteInfo useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics); - ConstantValue matchPossible = ExpressionOfTypeMatchesPatternType( - Conversions, inputType, patternType, ref useSiteInfo, out Conversion conversion, operandConstantValue: null, operandCouldBeNull: true); + matchPossible = ExpressionOfTypeMatchesPatternType( + Conversions, inputType, patternType, ref useSiteInfo, out conversion, operandConstantValue: null, operandCouldBeNull: true); diagnostics.Add(typeSyntax, useSiteInfo); + + if (reportBadMatch(typeSyntax, inputType, patternType, matchPossible, conversion, diagnostics)) + { + return true; + } + + if (unionType is not null) + { + useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics); + matchPossible = ExpressionOfTypeMatchesUnionPatternType( + Conversions, unionType, patternType, ref useSiteInfo, out conversion, operandConstantValue: null, operandCouldBeNull: true); + diagnostics.Add(typeSyntax, useSiteInfo); + + if (reportBadMatch(typeSyntax, unionType, patternType, matchPossible, conversion, diagnostics)) + { + return true; + } + } + } + + return false; + + bool reportBadMatch(SyntaxNode typeSyntax, TypeSymbol inputType, TypeSymbol patternType, ConstantValue matchPossible, Conversion conversion, BindingDiagnosticBag diagnostics) + { if (matchPossible != ConstantValue.False && matchPossible != ConstantValue.Bad) { if (!conversion.Exists && (inputType.ContainsTypeParameter() || patternType.ContainsTypeParameter())) @@ -798,9 +1118,36 @@ private bool CheckValidPatternType( Error(diagnostics, ErrorCode.ERR_PatternWrongType, typeSyntax, inputType, patternType); return true; } + + return false; } + } - return false; + internal static ConstantValue ExpressionOfTypeMatchesUnionPatternType( + Conversions conversions, + NamedTypeSymbol unionType, + TypeSymbol patternType, + ref CompoundUseSiteInfo useSiteInfo, + out Conversion conversion, + ConstantValue? operandConstantValue = null, + bool operandCouldBeNull = false) + { + ConstantValue matchPossible = ConstantValue.Bad; + conversion = Conversion.NoConversion; + + foreach (var caseType in unionType.UnionCaseTypes) + { + matchPossible = ExpressionOfTypeMatchesPatternType( + conversions, caseType, patternType, ref useSiteInfo, out conversion, + operandConstantValue, operandCouldBeNull); + + if (matchPossible != ConstantValue.False && matchPossible != ConstantValue.Bad) + { + return matchPossible; + } + } + + return matchPossible; } /// @@ -812,7 +1159,7 @@ private bool CheckValidPatternType( /// - 'null' if it might catch some of them. /// internal static ConstantValue ExpressionOfTypeMatchesPatternType( - Conversions conversions, + ConversionsBase conversions, TypeSymbol expressionType, TypeSymbol patternType, ref CompoundUseSiteInfo useSiteInfo, @@ -840,28 +1187,34 @@ internal static ConstantValue ExpressionOfTypeMatchesPatternType( ConstantValue result = Binder.GetIsOperatorConstantResult(expressionType, patternType, conversion.Kind, operandConstantValue, operandCouldBeNull); // Don't need to worry about checked user-defined operators - Debug.Assert(!conversion.IsUserDefined || result == ConstantValue.False || result == ConstantValue.Bad); + Debug.Assert((!conversion.IsUserDefined && !conversion.IsUnion) || result == ConstantValue.False || result == ConstantValue.Bad); return result; } private BoundPattern BindDeclarationPattern( DeclarationPatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool permitDesignations, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { + NamedTypeSymbol? unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + hasUnionMatching = unionMatchingInputType is not null; + TypeSyntax typeSyntax = node.Type; - BoundTypeExpression boundDeclType = BindTypeForPattern(typeSyntax, inputType, diagnostics, ref hasErrors); + BoundTypeExpression boundDeclType = BindTypeForPattern(typeSyntax, unionType, inputType, diagnostics, ref hasErrors); BindPatternDesignation( designation: node.Designation, declType: boundDeclType.TypeWithAnnotations, permitDesignations, typeSyntax, diagnostics, hasErrors: ref hasErrors, variableSymbol: out Symbol? variableSymbol, variableAccess: out BoundExpression? variableAccess); - return new BoundDeclarationPattern(node, boundDeclType, isVar: false, variableSymbol, variableAccess, inputType: inputType, narrowedType: boundDeclType.Type, hasErrors); + return new BoundDeclarationPattern(node, boundDeclType, isVar: false, variableSymbol, variableAccess, isUnionMatching: hasUnionMatching, inputType: unionMatchingInputType ?? inputType, narrowedType: boundDeclType.Type, hasErrors); } private BoundTypeExpression BindTypeForPattern( TypeSyntax typeSyntax, + NamedTypeSymbol? unionType, TypeSymbol inputType, BindingDiagnosticBag diagnostics, ref bool hasErrors) @@ -870,7 +1223,7 @@ private BoundTypeExpression BindTypeForPattern( TypeWithAnnotations declType = BindType(typeSyntax, diagnostics, out AliasSymbol aliasOpt); Debug.Assert(declType.HasType); BoundTypeExpression boundDeclType = new BoundTypeExpression(typeSyntax, aliasOpt, typeWithAnnotations: declType); - hasErrors |= CheckValidPatternType(typeSyntax, inputType, declType.Type, diagnostics: diagnostics); + hasErrors |= CheckValidPatternType(typeSyntax, unionType, inputType, declType.Type, diagnostics: diagnostics); return boundDeclType; } @@ -937,6 +1290,7 @@ private void BindPatternDesignation( private TypeWithAnnotations BindRecursivePatternType( TypeSyntax? typeSyntax, + NamedTypeSymbol? unionType, TypeSymbol inputType, BindingDiagnosticBag diagnostics, ref bool hasErrors, @@ -944,7 +1298,7 @@ private TypeWithAnnotations BindRecursivePatternType( { if (typeSyntax != null) { - boundDeclType = BindTypeForPattern(typeSyntax, inputType, diagnostics, ref hasErrors); + boundDeclType = BindTypeForPattern(typeSyntax, unionType, inputType, diagnostics, ref hasErrors); return boundDeclType.TypeWithAnnotations; } else @@ -967,11 +1321,17 @@ internal static bool IsZeroElementTupleType(TypeSymbol type) private BoundPattern BindRecursivePattern( RecursivePatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool permitDesignations, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { + NamedTypeSymbol? unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + bool isUnionMatching = unionMatchingInputType is not null; + hasUnionMatching = isUnionMatching; + MessageID.IDS_FeatureRecursivePatterns.CheckFeatureAvailability(diagnostics, node); if (inputType.IsPointerOrFunctionPointer()) @@ -982,7 +1342,7 @@ private BoundPattern BindRecursivePattern( } TypeSyntax? typeSyntax = node.Type; - TypeWithAnnotations declTypeWithAnnotations = BindRecursivePatternType(typeSyntax, inputType, diagnostics, ref hasErrors, out BoundTypeExpression? boundDeclType); + TypeWithAnnotations declTypeWithAnnotations = BindRecursivePatternType(typeSyntax, unionType, inputType, diagnostics, ref hasErrors, out BoundTypeExpression? boundDeclType); TypeSymbol declType = declTypeWithAnnotations.Type; MethodSymbol? deconstructMethod = null; @@ -997,12 +1357,14 @@ private BoundPattern BindRecursivePattern( // do not correctly treat the non-generic struct `System.ValueTuple` as a tuple type. We explicitly perform the tests // required to identify it. When that bug is fixed we should be able to remove this if statement. BindValueTupleSubpatterns( - positionalClause, declType, ImmutableArray.Empty, permitDesignations, ref hasErrors, patternsBuilder, diagnostics); + positionalClause, declType, ImmutableArray.Empty, permitDesignations, ref hasErrors, patternsBuilder, diagnostics, out bool tupleHasUnionMatching); + hasUnionMatching |= tupleHasUnionMatching; } else if (declType.IsTupleType) { // It is a tuple type. Work according to its elements - BindValueTupleSubpatterns(positionalClause, declType, declType.TupleElementTypesWithAnnotations, permitDesignations, ref hasErrors, patternsBuilder, diagnostics); + BindValueTupleSubpatterns(positionalClause, declType, declType.TupleElementTypesWithAnnotations, permitDesignations, ref hasErrors, patternsBuilder, diagnostics, out bool tupleHasUnionMatching); + hasUnionMatching |= tupleHasUnionMatching; } else { @@ -1019,9 +1381,10 @@ private BoundPattern BindRecursivePattern( // There was no Deconstruct, but the constraints for the use of ITuple are satisfied. // Use that and forget any errors from trying to bind Deconstruct. deconstructDiagnostics.Free(); - BindITupleSubpatterns(positionalClause, patternsBuilder, permitDesignations, diagnostics); + BindITupleSubpatterns(positionalClause, patternsBuilder, permitDesignations, diagnostics, out bool iTupleHasUnionMatching); + hasUnionMatching |= iTupleHasUnionMatching; deconstructionSubpatterns = patternsBuilder.ToImmutableAndFree(); - return new BoundITuplePattern(node, iTupleGetLength, iTupleGetItem, deconstructionSubpatterns, inputType, iTupleType, hasErrors); + return new BoundITuplePattern(node, iTupleGetLength, iTupleGetItem, deconstructionSubpatterns, isUnionMatching: isUnionMatching, inputType: unionMatchingInputType ?? inputType, iTupleType, hasErrors); } else { @@ -1029,7 +1392,8 @@ private BoundPattern BindRecursivePattern( } deconstructMethod = BindDeconstructSubpatterns( - positionalClause, permitDesignations, deconstruct, outPlaceholders, patternsBuilder, ref hasErrors, diagnostics); + positionalClause, permitDesignations, deconstruct, outPlaceholders, patternsBuilder, ref hasErrors, diagnostics, out bool deconstructHasUnionMatching); + hasUnionMatching |= deconstructHasUnionMatching; } deconstructionSubpatterns = patternsBuilder.ToImmutableAndFree(); @@ -1038,7 +1402,8 @@ private BoundPattern BindRecursivePattern( ImmutableArray properties = default; if (node.PropertyPatternClause != null) { - properties = BindPropertyPatternClause(node.PropertyPatternClause, declType, permitDesignations, diagnostics, ref hasErrors); + properties = BindPropertyPatternClause(node.PropertyPatternClause, declType, permitDesignations, diagnostics, ref hasErrors, out bool clauseHasUnionMatching); + hasUnionMatching |= clauseHasUnionMatching; } BindPatternDesignation( @@ -1053,7 +1418,7 @@ deconstructMethod is null && return new BoundRecursivePattern( syntax: node, declaredType: boundDeclType, deconstructMethod: deconstructMethod, deconstruction: deconstructionSubpatterns, properties: properties, isExplicitNotNullTest: isExplicitNotNullTest, - variable: variableSymbol, variableAccess: variableAccess, inputType: inputType, + variable: variableSymbol, variableAccess: variableAccess, isUnionMatching: isUnionMatching, inputType: unionMatchingInputType ?? inputType, narrowedType: boundDeclType?.Type ?? inputType.StrippedType(), hasErrors); } @@ -1064,12 +1429,14 @@ deconstructMethod is null && ImmutableArray outPlaceholders, ArrayBuilder patterns, ref bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { var deconstructMethod = deconstruct.ExpressionSymbol as MethodSymbol; if (deconstructMethod is null) hasErrors = true; + hasUnionMatching = false; int skippedExtensionParameters = deconstructMethod?.IsExtensionMethod == true ? 1 : 0; for (int i = 0; i < node.Subpatterns.Count; i++) { @@ -1106,11 +1473,13 @@ deconstructMethod is null && } } + NamedTypeSymbol? unionType = null; var boundSubpattern = new BoundPositionalSubpattern( subPattern, parameter, - BindPattern(subPattern.Pattern, elementType, permitDesignations, isError, diagnostics) + BindPattern(subPattern.Pattern, ref unionType, elementType, permitDesignations, isError, diagnostics, out bool subPatternHasUnionMatching) ); + hasUnionMatching |= subPatternHasUnionMatching; patterns.Add(boundSubpattern); } @@ -1121,8 +1490,10 @@ private void BindITupleSubpatterns( PositionalPatternClauseSyntax node, ArrayBuilder patterns, bool permitDesignations, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { + hasUnionMatching = false; var objectType = Compilation.GetSpecialType(SpecialType.System_Object); foreach (var subpatternSyntax in node.Subpatterns) { @@ -1136,28 +1507,12 @@ private void BindITupleSubpatterns( diagnostics.Add(ErrorCode.ERR_IdentifierExpected, subpatternSyntax.ExpressionColon.Expression.Location); } + NamedTypeSymbol? unionType = null; var boundSubpattern = new BoundPositionalSubpattern( subpatternSyntax, null, - BindPattern(subpatternSyntax.Pattern, objectType, permitDesignations, hasErrors: false, diagnostics)); - patterns.Add(boundSubpattern); - } - } - - private void BindITupleSubpatterns( - ParenthesizedVariableDesignationSyntax node, - ArrayBuilder patterns, - bool permitDesignations, - BindingDiagnosticBag diagnostics) - { - var objectType = Compilation.GetSpecialType(SpecialType.System_Object); - foreach (var variable in node.Variables) - { - BoundPattern pattern = BindVarDesignation(variable, objectType, permitDesignations, hasErrors: false, diagnostics); - var boundSubpattern = new BoundPositionalSubpattern( - variable, - null, - pattern); + BindPattern(subpatternSyntax.Pattern, ref unionType, objectType, permitDesignations, hasErrors: false, diagnostics, out bool subPatternHasUnionMatching)); + hasUnionMatching |= subPatternHasUnionMatching; patterns.Add(boundSubpattern); } } @@ -1169,7 +1524,8 @@ private void BindValueTupleSubpatterns( bool permitDesignations, ref bool hasErrors, ArrayBuilder patterns, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { if (elementTypesWithAnnotations.Length != node.Subpatterns.Count && !hasErrors) { @@ -1177,6 +1533,7 @@ private void BindValueTupleSubpatterns( hasErrors = true; } + hasUnionMatching = false; for (int i = 0; i < node.Subpatterns.Count; i++) { var subpatternSyntax = node.Subpatterns[i]; @@ -1196,10 +1553,12 @@ private void BindValueTupleSubpatterns( } } + NamedTypeSymbol? unionType = null; BoundPositionalSubpattern boundSubpattern = new BoundPositionalSubpattern( subpatternSyntax, foundField, - BindPattern(subpatternSyntax.Pattern, elementType, permitDesignations, isError, diagnostics)); + BindPattern(subpatternSyntax.Pattern, ref unionType, elementType, permitDesignations, isError, diagnostics, out bool subPatternHasUnionMatching)); + hasUnionMatching |= subPatternHasUnionMatching; patterns.Add(boundSubpattern); } } @@ -1291,6 +1650,10 @@ private bool ShouldUseITuple( diagnostics.ReportUseSite(iTupleGetLength, node) || diagnostics.ReportUseSite(iTupleGetItem, node); + AssertNotUnsafeMemberAccess(iTupleType); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, iTupleGetLength, node); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, iTupleGetItem, node); + return true; bool hasBaseInterface(TypeSymbol type, NamedTypeSymbol possibleBaseInterface) @@ -1327,10 +1690,12 @@ bool hasBaseInterface(TypeSymbol type, NamedTypeSymbol possibleBaseInterface) private BoundPattern BindVarPattern( VarPatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool permitDesignations, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { if ((inputType.IsPointerOrFunctionPointer() && node.Designation.Kind() == SyntaxKind.ParenthesizedVariableDesignation) || (inputType.IsPointerType() && Compilation.LanguageVersion < MessageID.IDS_FeatureRecursivePatterns.RequiredVersion())) @@ -1348,20 +1713,25 @@ private BoundPattern BindVarPattern( hasErrors = true; } - return BindVarDesignation(node.Designation, inputType, permitDesignations, hasErrors, diagnostics); + return BindVarDesignation(node.Designation, ref unionType, inputType, permitDesignations, hasErrors, diagnostics, out hasUnionMatching); } private BoundPattern BindVarDesignation( VariableDesignationSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool permitDesignations, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { switch (node.Kind()) { case SyntaxKind.DiscardDesignation: { + hasUnionMatching = false; + // https://github.com/dotnet/roslyn/issues/82636: Add test coverage for a Union type + // https://github.com/dotnet/roslyn/issues/82636: Add test coverage for 'unionType' not changing return new BoundDiscardPattern(node, inputType: inputType, narrowedType: inputType); } case SyntaxKind.SingleVariableDesignation: @@ -1374,96 +1744,135 @@ private BoundPattern BindVarDesignation( var boundOperandType = new BoundTypeExpression(syntax: node, aliasOpt: null, typeWithAnnotations: declType); // fake a type expression for the variable's type // We continue to use a BoundDeclarationPattern for the var pattern, as they have more in common. Debug.Assert(node.Parent is { }); + + hasUnionMatching = false; + // https://github.com/dotnet/roslyn/issues/82636: Add test coverage for 'unionType' not changing return new BoundDeclarationPattern( node.Parent.Kind() == SyntaxKind.VarPattern ? node.Parent : node, // for `var x` use whole pattern, otherwise use designation for the syntax boundOperandType, isVar: true, variableSymbol, variableAccess, - inputType: inputType, narrowedType: inputType, hasErrors); + isUnionMatching: false, inputType: inputType, narrowedType: inputType, hasErrors); } case SyntaxKind.ParenthesizedVariableDesignation: { - MessageID.IDS_FeatureRecursivePatterns.CheckFeatureAvailability(diagnostics, node); + return bindParenthesizedVariableDesignation(node, ref unionType, inputType, permitDesignations, hasErrors, diagnostics, out hasUnionMatching); + } + default: + { + throw ExceptionUtilities.UnexpectedValue(node.Kind()); + } + } - var tupleDesignation = (ParenthesizedVariableDesignationSyntax)node; - var subPatterns = ArrayBuilder.GetInstance(tupleDesignation.Variables.Count); - MethodSymbol? deconstructMethod = null; - var strippedInputType = inputType.StrippedType(); + BoundPattern bindParenthesizedVariableDesignation(VariableDesignationSyntax node, ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool permitDesignations, bool hasErrors, BindingDiagnosticBag diagnostics, out bool hasUnionMatching) + { + NamedTypeSymbol? unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + bool isUnionMatching = unionMatchingInputType is not null; + hasUnionMatching = isUnionMatching; - if (IsZeroElementTupleType(strippedInputType)) - { - // Work around https://github.com/dotnet/roslyn/issues/20648: The compiler's internal APIs such as `declType.IsTupleType` - // do not correctly treat the non-generic struct `System.ValueTuple` as a tuple type. We explicitly perform the tests - // required to identify it. When that bug is fixed we should be able to remove this if statement. - addSubpatternsForTuple(ImmutableArray.Empty); - } - else if (strippedInputType.IsTupleType) - { - // It is a tuple type. Work according to its elements - addSubpatternsForTuple(strippedInputType.TupleElementTypesWithAnnotations); - } - else - { - // It is not a tuple type. Seek an appropriate Deconstruct method. - var inputPlaceholder = new BoundImplicitReceiver(node, strippedInputType); // A fake receiver expression to permit us to reuse binding logic - var deconstructDiagnostics = BindingDiagnosticBag.GetInstance(diagnostics); - BoundExpression deconstruct = MakeDeconstructInvocationExpression( - tupleDesignation.Variables.Count, inputPlaceholder, node, deconstructDiagnostics, - outPlaceholders: out ImmutableArray outPlaceholders, - out bool anyDeconstructCandidates); - if (!anyDeconstructCandidates && - ShouldUseITuple(node, strippedInputType, diagnostics, out var iTupleType, out var iTupleGetLength, out var iTupleGetItem)) - { - // There was no applicable candidate Deconstruct, and the constraints for the use of ITuple are satisfied. - // Use that and forget any errors from trying to bind Deconstruct. - deconstructDiagnostics.Free(); - BindITupleSubpatterns(tupleDesignation, subPatterns, permitDesignations, diagnostics); - return new BoundITuplePattern(node, iTupleGetLength, iTupleGetItem, subPatterns.ToImmutableAndFree(), strippedInputType, iTupleType, hasErrors); - } - else - { - diagnostics.AddRangeAndFree(deconstructDiagnostics); - } + MessageID.IDS_FeatureRecursivePatterns.CheckFeatureAvailability(diagnostics, node); - deconstructMethod = deconstruct.ExpressionSymbol as MethodSymbol; - if (!hasErrors) - hasErrors = outPlaceholders.IsDefault || tupleDesignation.Variables.Count != outPlaceholders.Length; + var tupleDesignation = (ParenthesizedVariableDesignationSyntax)node; + var subPatterns = ArrayBuilder.GetInstance(tupleDesignation.Variables.Count); + MethodSymbol? deconstructMethod = null; + var strippedInputType = inputType.StrippedType(); - for (int i = 0; i < tupleDesignation.Variables.Count; i++) - { - var variable = tupleDesignation.Variables[i]; - bool isError = outPlaceholders.IsDefaultOrEmpty || i >= outPlaceholders.Length; - TypeSymbol elementType = isError ? CreateErrorType() : outPlaceholders[i].Type; - BoundPattern pattern = BindVarDesignation(variable, elementType, permitDesignations, isError, diagnostics); - subPatterns.Add(new BoundPositionalSubpattern(variable, symbol: null, pattern)); - } - } + if (IsZeroElementTupleType(strippedInputType)) + { + // Work around https://github.com/dotnet/roslyn/issues/20648: The compiler's internal APIs such as `declType.IsTupleType` + // do not correctly treat the non-generic struct `System.ValueTuple` as a tuple type. We explicitly perform the tests + // required to identify it. When that bug is fixed we should be able to remove this if statement. + addSubpatternsForTuple(ImmutableArray.Empty, ref hasUnionMatching); + } + else if (strippedInputType.IsTupleType) + { + // It is a tuple type. Work according to its elements + addSubpatternsForTuple(strippedInputType.TupleElementTypesWithAnnotations, ref hasUnionMatching); + } + else + { + // It is not a tuple type. Seek an appropriate Deconstruct method. + var inputPlaceholder = new BoundImplicitReceiver(node, strippedInputType); // A fake receiver expression to permit us to reuse binding logic + var deconstructDiagnostics = BindingDiagnosticBag.GetInstance(diagnostics); + BoundExpression deconstruct = MakeDeconstructInvocationExpression( + tupleDesignation.Variables.Count, inputPlaceholder, node, deconstructDiagnostics, + outPlaceholders: out ImmutableArray outPlaceholders, + out bool anyDeconstructCandidates); + if (!anyDeconstructCandidates && + ShouldUseITuple(node, strippedInputType, diagnostics, out var iTupleType, out var iTupleGetLength, out var iTupleGetItem)) + { + // There was no applicable candidate Deconstruct, and the constraints for the use of ITuple are satisfied. + // Use that and forget any errors from trying to bind Deconstruct. + deconstructDiagnostics.Free(); + bindITupleSubpatterns(tupleDesignation, subPatterns, permitDesignations, diagnostics, out bool iTupleHasUnionMatching); + hasUnionMatching |= iTupleHasUnionMatching; + return new BoundITuplePattern(node, iTupleGetLength, iTupleGetItem, subPatterns.ToImmutableAndFree(), isUnionMatching: isUnionMatching, inputType: unionMatchingInputType ?? strippedInputType, iTupleType, hasErrors); + } + else + { + diagnostics.AddRangeAndFree(deconstructDiagnostics); + } - return new BoundRecursivePattern( - syntax: node, declaredType: null, deconstructMethod: deconstructMethod, - deconstruction: subPatterns.ToImmutableAndFree(), properties: default, variable: null, variableAccess: null, - isExplicitNotNullTest: false, inputType: inputType, narrowedType: inputType.StrippedType(), hasErrors: hasErrors); + deconstructMethod = deconstruct.ExpressionSymbol as MethodSymbol; + if (!hasErrors) + hasErrors = outPlaceholders.IsDefault || tupleDesignation.Variables.Count != outPlaceholders.Length; - void addSubpatternsForTuple(ImmutableArray elementTypes) - { - if (elementTypes.Length != tupleDesignation.Variables.Count && !hasErrors) - { - diagnostics.Add(ErrorCode.ERR_WrongNumberOfSubpatterns, tupleDesignation.Location, - strippedInputType, elementTypes.Length, tupleDesignation.Variables.Count); - hasErrors = true; - } - for (int i = 0; i < tupleDesignation.Variables.Count; i++) - { - var variable = tupleDesignation.Variables[i]; - bool isError = i >= elementTypes.Length; - TypeSymbol elementType = isError ? CreateErrorType() : elementTypes[i].Type; - BoundPattern pattern = BindVarDesignation(variable, elementType, permitDesignations, isError, diagnostics); - subPatterns.Add(new BoundPositionalSubpattern(variable, symbol: null, pattern)); - } - } + for (int i = 0; i < tupleDesignation.Variables.Count; i++) + { + var variable = tupleDesignation.Variables[i]; + bool isError = outPlaceholders.IsDefaultOrEmpty || i >= outPlaceholders.Length; + TypeSymbol elementType = isError ? CreateErrorType() : outPlaceholders[i].Type; + NamedTypeSymbol? varUnionType = null; + BoundPattern pattern = BindVarDesignation(variable, ref varUnionType, elementType, permitDesignations, isError, diagnostics, out bool varHasUnionMatching); + hasUnionMatching |= varHasUnionMatching; + subPatterns.Add(new BoundPositionalSubpattern(variable, symbol: null, pattern)); } - default: + } + + return new BoundRecursivePattern( + syntax: node, declaredType: null, deconstructMethod: deconstructMethod, + deconstruction: subPatterns.ToImmutableAndFree(), properties: default, variable: null, variableAccess: null, + isExplicitNotNullTest: false, isUnionMatching: isUnionMatching, inputType: unionMatchingInputType ?? inputType, narrowedType: inputType.StrippedType(), hasErrors: hasErrors); + + void addSubpatternsForTuple(ImmutableArray elementTypes, ref bool hasUnionMatching) + { + if (elementTypes.Length != tupleDesignation.Variables.Count && !hasErrors) { - throw ExceptionUtilities.UnexpectedValue(node.Kind()); + diagnostics.Add(ErrorCode.ERR_WrongNumberOfSubpatterns, tupleDesignation.Location, + strippedInputType, elementTypes.Length, tupleDesignation.Variables.Count); + hasErrors = true; + } + for (int i = 0; i < tupleDesignation.Variables.Count; i++) + { + var variable = tupleDesignation.Variables[i]; + bool isError = i >= elementTypes.Length; + TypeSymbol elementType = isError ? CreateErrorType() : elementTypes[i].Type; + NamedTypeSymbol? unionType = null; + BoundPattern pattern = BindVarDesignation(variable, ref unionType, elementType, permitDesignations, isError, diagnostics, out bool varHasUnionMatching); + hasUnionMatching |= varHasUnionMatching; + subPatterns.Add(new BoundPositionalSubpattern(variable, symbol: null, pattern)); + } + } + + void bindITupleSubpatterns( + ParenthesizedVariableDesignationSyntax node, + ArrayBuilder patterns, + bool permitDesignations, + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) + { + var objectType = Compilation.GetSpecialType(SpecialType.System_Object); + hasUnionMatching = false; + foreach (var variable in node.Variables) + { + NamedTypeSymbol? unionType = null; + BoundPattern pattern = BindVarDesignation(variable, ref unionType, objectType, permitDesignations, hasErrors: false, diagnostics, out bool varHasUnionMatching); + hasUnionMatching |= varHasUnionMatching; + var boundSubpattern = new BoundPositionalSubpattern( + variable, + null, + pattern); + patterns.Add(boundSubpattern); } + } } } @@ -1472,9 +1881,11 @@ private ImmutableArray BindPropertyPatternClause( TypeSymbol inputType, bool permitDesignations, BindingDiagnosticBag diagnostics, - ref bool hasErrors) + ref bool hasErrors, + out bool hasUnionMatching) { var builder = ArrayBuilder.GetInstance(node.Subpatterns.Count); + hasUnionMatching = false; foreach (SubpatternSyntax p in node.Subpatterns) { if (p.ExpressionColon is ExpressionColonSyntax) @@ -1511,7 +1922,9 @@ private ImmutableArray BindPropertyPatternClause( } } - BoundPattern boundPattern = BindPattern(pattern, memberType, permitDesignations, hasErrors, diagnostics); + NamedTypeSymbol? unionType = null; + BoundPattern boundPattern = BindPattern(pattern, ref unionType, memberType, permitDesignations, hasErrors, diagnostics, out bool patternHasUnionMatching); + hasUnionMatching |= patternHasUnionMatching; builder.Add(new BoundPropertySubpattern(p, member, isLengthOrCount, boundPattern)); } @@ -1621,26 +2034,36 @@ private BoundPropertySubpatternMember LookupMembersForPropertyPattern( private BoundPattern BindTypePattern( TypePatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { + NamedTypeSymbol? unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + hasUnionMatching = unionMatchingInputType is not null; + MessageID.IDS_FeatureTypePattern.CheckFeatureAvailability(diagnostics, node); - var patternType = BindTypeForPattern(node.Type, inputType, diagnostics, ref hasErrors); + var patternType = BindTypeForPattern(node.Type, unionType, inputType, diagnostics, ref hasErrors); bool isExplicitNotNullTest = patternType.Type.SpecialType == SpecialType.System_Object; - return new BoundTypePattern(node, patternType, isExplicitNotNullTest, inputType, patternType.Type, hasErrors); + return new BoundTypePattern(node, patternType, isExplicitNotNullTest, isUnionMatching: hasUnionMatching, inputType: unionMatchingInputType ?? inputType, patternType.Type, hasErrors); } private BoundPattern BindRelationalPattern( RelationalPatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { + NamedTypeSymbol? unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + hasUnionMatching = unionMatchingInputType is not null; + MessageID.IDS_FeatureRelationalPattern.CheckFeatureAvailability(diagnostics, node.OperatorToken); - BoundExpression value = BindExpressionForPattern(inputType, node.Expression, ref hasErrors, diagnostics, out var constantValueOpt, out _, out Conversion patternConversion); + BoundExpression value = BindExpressionForPattern(unionType, inputType, node.Expression, ref hasErrors, diagnostics, out var constantValueOpt, out _, out Conversion patternConversion, originalExpression: out _); ExpressionSyntax innerExpression = SkipParensAndNullSuppressions(node.Expression, diagnostics, ref hasErrors); var type = value.Type ?? inputType; Debug.Assert(type is { }); @@ -1686,7 +2109,7 @@ private BoundPattern BindRelationalPattern( hasErrors = true; } - return new BoundRelationalPattern(node, operation | opType, value, constantValueOpt, inputType, type, hasErrors); + return new BoundRelationalPattern(node, operation | opType, value, constantValueOpt, isUnionMatching: hasUnionMatching, inputType: unionMatchingInputType ?? inputType, type, hasErrors); static BinaryOperatorKind tokenKindToBinaryOperatorKind(SyntaxKind kind) => kind switch { @@ -1727,24 +2150,44 @@ private BoundPattern BindRelationalPattern( private BoundPattern BindUnaryPattern( UnaryPatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool hasErrors, BindingDiagnosticBag diagnostics, - bool underIsPattern) + bool underIsPattern, + out bool hasUnionMatching) { + NamedTypeSymbol? unionMatchingInputType = PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + bool isUnionMatching = unionMatchingInputType is not null; + + if (unionMatchingInputType is not null && (unionMatchingInputType.TypeKind != TypeKind.Struct || unionMatchingInputType.IsNullableType())) + { + // When we are not 'underIsPattern', we don't 'permitDesignations'. See an assignment and a comment below. + // Only 'BindIsPatternExpression' passes true for 'underIsPattern' and only 'BindUnaryPattern' propagates it. + // Since we are doing Union matching, we are effectively in a sub-pattern, thus we are not directly under + // "is pattern", but we can still make things work for struct types (excluding Nullable), because they + // do not have an implicit null check for the Union instance itself. + // The effect of this logic can be observed in unit-tests 'UnionMatching_15_Negated' and 'UnionMatching_16_Negated', for example. + underIsPattern = false; + } + MessageID.IDS_FeatureNotPattern.CheckFeatureAvailability(diagnostics, node.OperatorToken); bool permitDesignations = underIsPattern; // prevent designators under 'not' except under an is-pattern - var subPattern = BindPattern(node.Pattern, inputType, permitDesignations, hasErrors, diagnostics, underIsPattern); - return new BoundNegatedPattern(node, subPattern, inputType: inputType, narrowedType: inputType, hasErrors); + NamedTypeSymbol? currentUnionType = unionType; + var subPattern = BindPattern(node.Pattern, ref currentUnionType, inputType, permitDesignations, hasErrors, diagnostics, out hasUnionMatching, underIsPattern); + hasUnionMatching |= isUnionMatching; + return new BoundNegatedPattern(node, subPattern, isUnionMatching: isUnionMatching, inputType: unionMatchingInputType ?? inputType, narrowedType: inputType, hasErrors); } private BoundPattern BindBinaryPattern( BinaryPatternSyntax node, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, bool permitDesignations, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + out bool hasUnionMatching) { // Users (such as ourselves) can have many, many nested binary patterns. To avoid crashing, do left recursion manually. @@ -1761,9 +2204,11 @@ private BoundPattern BindBinaryPattern( Debug.Assert(binaryPatternStack.Count > 0); var binaryPatternAndPermitDesignations = binaryPatternStack.Pop(); - BoundPattern result = BindPattern(binaryPatternAndPermitDesignations.pat.Left, inputType, binaryPatternAndPermitDesignations.permitDesignations, hasErrors, diagnostics); + NamedTypeSymbol? incomingUnionType = unionType; + BoundPattern result = BindPattern(binaryPatternAndPermitDesignations.pat.Left, ref unionType, inputType, binaryPatternAndPermitDesignations.permitDesignations, hasErrors, diagnostics, out hasUnionMatching); var narrowedTypeCandidates = ArrayBuilder.GetInstance(2); - collectCandidates(result, narrowedTypeCandidates); + + CollectDisjunctionTypes(result, narrowedTypeCandidates, hasUnionMatching); do { @@ -1772,25 +2217,31 @@ private BoundPattern BindBinaryPattern( this, binaryPatternAndPermitDesignations.pat, binaryPatternAndPermitDesignations.permitDesignations, + incomingUnionType, + ref unionType, inputType, narrowedTypeCandidates, hasErrors, - diagnostics); + diagnostics, + ref hasUnionMatching); } while (binaryPatternStack.TryPop(out binaryPatternAndPermitDesignations)); binaryPatternStack.Free(); narrowedTypeCandidates.Free(); return result; - static BoundPattern bindBinaryPattern( + static BoundBinaryPattern bindBinaryPattern( BoundPattern preboundLeft, Binder binder, BinaryPatternSyntax node, bool permitDesignations, + NamedTypeSymbol? incomingUnionType, + ref NamedTypeSymbol? unionType, TypeSymbol inputType, ArrayBuilder narrowedTypeCandidates, bool hasErrors, - BindingDiagnosticBag diagnostics) + BindingDiagnosticBag diagnostics, + ref bool hasUnionMatching) { bool isDisjunction = node.Kind() == SyntaxKind.OrPattern; if (isDisjunction) @@ -1798,93 +2249,134 @@ static BoundPattern bindBinaryPattern( Debug.Assert(!permitDesignations); MessageID.IDS_FeatureOrPattern.CheckFeatureAvailability(diagnostics, node.OperatorToken); - var right = binder.BindPattern(node.Right, inputType, permitDesignations, hasErrors, diagnostics); - - // Compute the common type. This algorithm is quadratic, but disjunctive patterns are unlikely to be huge - collectCandidates(right, narrowedTypeCandidates); - var narrowedType = leastSpecificType(node, narrowedTypeCandidates, diagnostics) ?? inputType; + unionType = incomingUnionType; + NamedTypeSymbol? rightUnionType = unionType; + var right = binder.BindPattern(node.Right, ref rightUnionType, inputType, permitDesignations, hasErrors, diagnostics, out bool rightHasUnionMatching); + hasUnionMatching |= rightHasUnionMatching; - return new BoundBinaryPattern(node, disjunction: isDisjunction, preboundLeft, right, inputType: inputType, narrowedType: narrowedType, hasErrors); + // Note, if we change how we compute the 'narrowedType' here, similar adjustments might be necessary in + // 'UnionMatchingRewriter.VisitBinaryPattern.rewriteBinaryPattern' - TypeSymbol? leastSpecificType(SyntaxNode node, ArrayBuilder candidates, BindingDiagnosticBag diagnostics) - { - Debug.Assert(candidates.Count >= 2); - CompoundUseSiteInfo useSiteInfo = binder.GetNewCompoundUseSiteInfo(diagnostics); - TypeSymbol? bestSoFar = candidates[0]; - // first pass: select a candidate for which no other has been shown to be an improvement. - for (int i = 1, n = candidates.Count; i < n; i++) - { - TypeSymbol candidate = candidates[i]; - bestSoFar = lessSpecificCandidate(bestSoFar, candidate, ref useSiteInfo) ?? bestSoFar; - } - // second pass: check that it is no more specific than any candidate. - for (int i = 0, n = candidates.Count; i < n; i++) - { - TypeSymbol candidate = candidates[i]; - TypeSymbol? spoiler = lessSpecificCandidate(candidate, bestSoFar, ref useSiteInfo); - if (spoiler is null) - { - bestSoFar = null; - break; - } - - // Our specificity criteria are transitive - Debug.Assert(spoiler.Equals(bestSoFar, TypeCompareKind.ConsiderEverything)); - } - - diagnostics.Add(node, useSiteInfo); - return bestSoFar; - } + // Compute the common type. This algorithm is quadratic, but disjunctive patterns are unlikely to be huge + CollectDisjunctionTypes(right, narrowedTypeCandidates, rightHasUnionMatching); + CompoundUseSiteInfo useSiteInfo = binder.GetNewCompoundUseSiteInfo(diagnostics); + var narrowedType = LeastSpecificType(narrowedTypeCandidates, binder.Conversions, ref useSiteInfo) ?? inputType; + diagnostics.Add(node, useSiteInfo); - // Given a candidate least specific type so far, attempt to refine it with a possibly less specific candidate. - TypeSymbol? lessSpecificCandidate(TypeSymbol bestSoFar, TypeSymbol possiblyLessSpecificCandidate, ref CompoundUseSiteInfo useSiteInfo) - { - if (bestSoFar.Equals(possiblyLessSpecificCandidate, TypeCompareKind.AllIgnoreOptions)) - { - // When the types are equivalent, merge them. - return bestSoFar.MergeEquivalentTypes(possiblyLessSpecificCandidate, VarianceKind.Out); - } - else if (binder.Conversions.HasImplicitReferenceConversion(bestSoFar, possiblyLessSpecificCandidate, ref useSiteInfo)) - { - // When there is an implicit reference conversion from T to U, U is less specific - return possiblyLessSpecificCandidate; - } - else if (binder.Conversions.HasBoxingConversion(bestSoFar, possiblyLessSpecificCandidate, ref useSiteInfo)) - { - // when there is a boxing conversion from T to U, U is less specific. - return possiblyLessSpecificCandidate; - } - else - { - // We have no improved candidate to offer. - return null; - } - } + return new BoundBinaryPattern(node, disjunction: true, preboundLeft, right, inputType: inputType, narrowedType: narrowedType, hasErrors); } else { MessageID.IDS_FeatureAndPattern.CheckFeatureAvailability(diagnostics, node.OperatorToken); - var right = binder.BindPattern(node.Right, preboundLeft.NarrowedType, permitDesignations, hasErrors, diagnostics); + var right = binder.BindPattern(node.Right, ref unionType, preboundLeft.NarrowedType, permitDesignations, hasErrors, diagnostics, out bool rightHasUnionMatching); + hasUnionMatching |= rightHasUnionMatching; + + var result = new BoundBinaryPattern(node, disjunction: isDisjunction, preboundLeft, right, inputType: inputType, narrowedType: right.NarrowedType, hasErrors); narrowedTypeCandidates.Clear(); - narrowedTypeCandidates.Add(right.NarrowedType); - return new BoundBinaryPattern(node, disjunction: isDisjunction, preboundLeft, right, inputType: inputType, narrowedType: right.NarrowedType, hasErrors); + CollectDisjunctionTypes(result, narrowedTypeCandidates, hasUnionMatching); + return result; + } + } + } + + internal static TypeSymbol? LeastSpecificType(ArrayBuilder candidates, Conversions conversions, ref CompoundUseSiteInfo useSiteInfo) + { + Debug.Assert(candidates.Count >= 2); + TypeSymbol? bestSoFar = candidates[0]; + // first pass: select a candidate for which no other has been shown to be an improvement. + for (int i = 1, n = candidates.Count; i < n; i++) + { + TypeSymbol candidate = candidates[i]; + bestSoFar = lessSpecificCandidate(bestSoFar, candidate, conversions, ref useSiteInfo) ?? bestSoFar; + } + // second pass: check that it is no more specific than any candidate. + for (int i = 0, n = candidates.Count; i < n; i++) + { + TypeSymbol candidate = candidates[i]; + TypeSymbol? spoiler = lessSpecificCandidate(candidate, bestSoFar, conversions, ref useSiteInfo); + if (spoiler is null) + { + bestSoFar = null; + break; } + + // Our specificity criteria are transitive + Debug.Assert(spoiler.Equals(bestSoFar, TypeCompareKind.ConsiderEverything)); } - static void collectCandidates(BoundPattern pat, ArrayBuilder candidates) + return bestSoFar; + + // Given a candidate least specific type so far, attempt to refine it with a possibly less specific candidate. + static TypeSymbol? lessSpecificCandidate(TypeSymbol bestSoFar, TypeSymbol possiblyLessSpecificCandidate, Conversions conversions, ref CompoundUseSiteInfo useSiteInfo) { - if (pat is BoundBinaryPattern { Disjunction: true } p) + if (bestSoFar.Equals(possiblyLessSpecificCandidate, TypeCompareKind.AllIgnoreOptions)) + { + // When the types are equivalent, merge them. + return bestSoFar.MergeEquivalentTypes(possiblyLessSpecificCandidate, VarianceKind.Out); + } + else if (conversions.HasImplicitReferenceConversion(bestSoFar, possiblyLessSpecificCandidate, ref useSiteInfo)) { - collectCandidates(p.Left, candidates); - collectCandidates(p.Right, candidates); + // When there is an implicit reference conversion from T to U, U is less specific + return possiblyLessSpecificCandidate; + } + else if (conversions.HasBoxingConversion(bestSoFar, possiblyLessSpecificCandidate, ref useSiteInfo)) + { + // when there is a boxing conversion from T to U, U is less specific. + return possiblyLessSpecificCandidate; } else { - candidates.Add(pat.NarrowedType); + // We have no improved candidate to offer. + return null; } } + } + internal static void CollectDisjunctionTypes(BoundPattern pat, ArrayBuilder candidates, bool hasUnionMatching) + { + while (pat is BoundBinaryPattern { Disjunction: true } p) + { + CollectDisjunctionTypes(p.Right, candidates, hasUnionMatching); + pat = p.Left; + } + + var candidate = pat.NarrowedType; + + if (hasUnionMatching) + { + adjustForUnionMatching(pat, ref candidate); + } + + candidates.Add(candidate); + + // Replace 'candidate' with the Union type for the union matching pattern + // at the top, or for the first union matching pattern in conjunction's leaves + // in evaluation order. + // Since all conjunctions, if any, starting with that union matching pattern are + // actually a Value property sub-pattern, they don't really narrow the type for the + // purposes of a disjunction performed outside that sub-pattern. + static void adjustForUnionMatching(BoundPattern pat, ref TypeSymbol candidate) + { + while (true) + { + if (pat is { IsUnionMatching: true }) + { + candidate = pat.InputType; + return; + } + + if (pat is BoundBinaryPattern { Disjunction: false } p) + { + adjustForUnionMatching(p.Right, ref candidate); + pat = p.Left; + } + else + { + return; + } + } + } } } } diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs index af94974d4c27..66a4b718cab4 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Statements.cs @@ -188,7 +188,7 @@ private BoundStatement BindFixedStatement(FixedStatementSyntax node, BindingDiag var fixedBinder = this.GetBinder(node); Debug.Assert(fixedBinder != null); - fixedBinder.ReportUnsafeIfNotAllowed(node, diagnostics); + fixedBinder.ReportUnsafeIfNotAllowed(node, diagnostics, disallowedUnder: MemorySafetyRules.Legacy); return fixedBinder.BindFixedStatementParts(node, diagnostics); } @@ -3183,6 +3183,7 @@ internal BoundExpression CreateReturnConversion( Conversion conversion; bool badAsyncReturnAlreadyReported = false; + bool hasImplicitConversionError = false; CompoundUseSiteInfo useSiteInfo = GetNewCompoundUseSiteInfo(diagnostics); if (IsInAsyncMethod()) { @@ -3233,7 +3234,12 @@ internal BoundExpression CreateReturnConversion( } else { - GenerateImplicitConversionError(diagnostics, argument.Syntax, conversion, argument, returnType); + var conversionDiagnostics = BindingDiagnosticBag.GetInstance(diagnostics); + GenerateImplicitConversionError(conversionDiagnostics, argument.Syntax, conversion, argument, returnType); + + hasImplicitConversionError = conversionDiagnostics.AccumulatesDiagnostics && conversionDiagnostics.HasAnyResolvedErrors(); + diagnostics.AddRangeAndFree(conversionDiagnostics); + if (this.ContainingMemberOrLambda is LambdaSymbol) { ReportCantConvertLambdaReturn(argument.Syntax, diagnostics); @@ -3243,7 +3249,17 @@ internal BoundExpression CreateReturnConversion( } } - return CreateConversion(argument.Syntax, argument, conversion, isCast: false, conversionGroupOpt: null, InConversionGroupFlags.Unspecified, returnType, diagnostics); + return CreateConversion( + argument.Syntax, + argument, + conversion, + isCast: false, + conversionGroupOpt: null, + InConversionGroupFlags.Unspecified, + returnType, + hasImplicitConversionError + ? BindingDiagnosticBag.Discarded + : diagnostics); } private BoundTryStatement BindTryStatement(TryStatementSyntax node, BindingDiagnosticBag diagnostics) @@ -3773,13 +3789,19 @@ private BoundNode BindConstructorBody(ConstructorDeclarationSyntax constructor, bool thisInitializer = initializer?.IsKind(SyntaxKind.ThisConstructorInitializer) == true; if (!thisInitializer && - hasPrimaryConstructor()) + isInstanceConstructor(out MethodSymbol constructorSymbol)) { - if (isInstanceConstructor(out MethodSymbol constructorSymbol) && - !SynthesizedRecordCopyCtor.IsCopyConstructor(constructorSymbol)) + if (hasPrimaryConstructor()) { - // Note: we check the constructor initializer of copy constructors elsewhere - Error(diagnostics, ErrorCode.ERR_UnexpectedOrMissingConstructorInitializerInRecord, initializer?.ThisOrBaseKeyword ?? constructor.Identifier); + if (!SynthesizedRecordCopyCtor.IsCopyConstructor(constructorSymbol)) + { + // Note: we check the constructor initializer of copy constructors elsewhere + Error(diagnostics, ErrorCode.ERR_UnexpectedOrMissingConstructorInitializerInRecord, initializer?.ThisOrBaseKeyword ?? constructor.Identifier); + } + } + else if (ContainingType is SourceMemberContainerTypeSymbol { IsUnionDeclaration: true }) + { + Error(diagnostics, ErrorCode.ERR_UnionConstructorCallsDefaultConstructor, initializer?.ThisOrBaseKeyword ?? constructor.Identifier); } } @@ -3787,10 +3809,16 @@ private BoundNode BindConstructorBody(ConstructorDeclarationSyntax constructor, && ContainingType.IsDefaultValueTypeConstructor(initializer); if (isDefaultValueTypeInitializer && - isInstanceConstructor(out _) && - hasPrimaryConstructor()) + isInstanceConstructor(out _)) { - Error(diagnostics, ErrorCode.ERR_RecordStructConstructorCallsDefaultConstructor, initializer.ThisOrBaseKeyword); + if (hasPrimaryConstructor()) + { + Error(diagnostics, ErrorCode.ERR_RecordStructConstructorCallsDefaultConstructor, initializer.ThisOrBaseKeyword); + } + else if (ContainingType is SourceMemberContainerTypeSymbol { IsUnionDeclaration: true }) + { + Error(diagnostics, ErrorCode.ERR_UnionConstructorCallsDefaultConstructor, initializer.ThisOrBaseKeyword); + } } // Using BindStatement to bind block to make sure we are reusing results of partial binding in SemanticModel diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Symbols.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Symbols.cs index 01bfdb547dbb..09f8a710183b 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Symbols.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Symbols.cs @@ -268,6 +268,7 @@ private NamespaceOrTypeOrAliasSymbolWithAnnotations BindTypeOrAliasOrKeyword(Syn if (symbol.Kind != SymbolKind.Alias) { ReportDiagnosticsIfObsolete(diagnostics, type, syntax, hasBaseReceiver: false); + AssertNotUnsafeMemberAccess(type); } } else @@ -334,6 +335,7 @@ internal NamespaceOrTypeOrAliasSymbolWithAnnotations BindTypeOrAlias(ExpressionS // Obsolete alias targets are reported in UnwrapAlias, but if it was a type (not an // alias to a type) we report the obsolete type here. symbol.TypeWithAnnotations.ReportDiagnosticsIfObsolete(this, syntax, diagnostics); + if (symbol.TypeWithAnnotations.IsResolved) ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, symbol.TypeWithAnnotations.Type, syntax); } return symbol; @@ -458,7 +460,7 @@ internal NamespaceOrTypeOrAliasSymbolWithAnnotations BindNamespaceOrTypeOrAliasS var functionPointerTypeSyntax = (FunctionPointerTypeSyntax)syntax; MessageID.IDS_FeatureFunctionPointers.CheckFeatureAvailability(diagnostics, functionPointerTypeSyntax.DelegateKeyword); - if (GetUnsafeDiagnosticInfo(sizeOfTypeOpt: null) is CSDiagnosticInfo info) + if (GetUnsafeDiagnosticInfo(disallowedUnder: MemorySafetyRules.Legacy, sizeOfTypeOpt: null) is CSDiagnosticInfo info) { var @delegate = functionPointerTypeSyntax.DelegateKeyword; var asterisk = functionPointerTypeSyntax.AsteriskToken; @@ -601,7 +603,7 @@ NamespaceOrTypeOrAliasSymbolWithAnnotations bindPointer() { var node = (PointerTypeSyntax)syntax; var elementType = BindType(node.ElementType, diagnostics, basesBeingResolved); - ReportUnsafeIfNotAllowed(node, diagnostics); + ReportUnsafeIfNotAllowed(node, diagnostics, disallowedUnder: MemorySafetyRules.Legacy); if (!Flags.HasFlag(BinderFlags.SuppressConstraintChecks)) { @@ -927,7 +929,7 @@ protected NamespaceOrTypeOrAliasSymbolWithAnnotations BindNonGenericSimpleNamesp if (type.ContainsPointerOrFunctionPointer()) { - ReportUnsafeIfNotAllowed(node, diagnostics); + ReportUnsafeIfNotAllowed(node, diagnostics, disallowedUnder: MemorySafetyRules.Legacy); } } } @@ -1146,6 +1148,7 @@ private Symbol UnwrapAlias(Symbol symbol, out AliasSymbol alias, BindingDiagnost type.VisitType((typePart, argTuple, isNested) => { argTuple.Item1.ReportDiagnosticsIfObsolete(argTuple.diagnostics, typePart, argTuple.syntax, hasBaseReceiver: false); + // Unsafe member access is reported on the `using` declaration. return false; }, args); } @@ -1667,6 +1670,7 @@ private NamespaceOrTypeOrAliasSymbolWithAnnotations BindQualifiedName( { var left = BindNamespaceOrTypeSymbol(leftName, diagnostics, basesBeingResolved, suppressUseSiteDiagnostics: false).NamespaceOrTypeSymbol; ReportDiagnosticsIfObsolete(diagnostics, left, leftName, hasBaseReceiver: false); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, left, leftName); bool isLeftUnboundGenericType = left.Kind == SymbolKind.NamedType && ((NamedTypeSymbol)left).IsUnboundGenericType; diff --git a/src/Compilers/CSharp/Portable/Binder/Binder_Unsafe.cs b/src/Compilers/CSharp/Portable/Binder/Binder_Unsafe.cs index 2c4d9b6a7fb8..9d0d346ec801 100644 --- a/src/Compilers/CSharp/Portable/Binder/Binder_Unsafe.cs +++ b/src/Compilers/CSharp/Portable/Binder/Binder_Unsafe.cs @@ -2,9 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - +using System; using System.Diagnostics; +using System.Linq; using Microsoft.CodeAnalysis.CSharp.Symbols; namespace Microsoft.CodeAnalysis.CSharp @@ -23,48 +23,268 @@ internal bool InUnsafeRegion get { return this.Flags.Includes(BinderFlags.UnsafeRegion); } } - /// True if a diagnostic was reported - internal bool ReportUnsafeIfNotAllowed(SyntaxNode node, BindingDiagnosticBag diagnostics, TypeSymbol sizeOfTypeOpt = null) + internal void ReportDiagnosticsIfUnsafeMemberAccess(BindingDiagnosticBag diagnostics, Symbol symbol, SyntaxNodeOrToken node) { - Debug.Assert((node.Kind() == SyntaxKind.SizeOfExpression) == ((object)sizeOfTypeOpt != null), "Should have a type for (only) sizeof expressions."); - var diagnosticInfo = GetUnsafeDiagnosticInfo(sizeOfTypeOpt); - if (diagnosticInfo == null) + if (diagnostics.DiagnosticBag is { } bag) { - return false; + ReportDiagnosticsIfUnsafeMemberAccess(bag, symbol, node, static node => node.GetLocation()); } + } - diagnostics.Add(new CSDiagnostic(diagnosticInfo, node.Location)); - return true; + internal void ReportDiagnosticsIfUnsafeMemberAccess(DiagnosticBag diagnostics, Symbol symbol, SyntaxNodeOrToken node) + { + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, symbol, node, static node => node.GetLocation()); + } + + internal void ReportDiagnosticsIfUnsafeMemberAccess(DiagnosticBag diagnostics, Symbol symbol, Location? location) + { + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, symbol, location, static l => l); } + private void ReportDiagnosticsIfUnsafeMemberAccess(DiagnosticBag diagnostics, Symbol symbol, T arg, Func location) + { + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, symbol, arg, location, forConstructorConstraint: false); + + if (ShouldCheckConstraints) + { + switch (symbol) + { + case MethodSymbol methodSymbol: + { + var arity = methodSymbol.GetMemberArityIncludingExtension(); + if (arity != 0) + { + var typeParameters = methodSymbol.GetTypeParametersIncludingExtension(); + var typeArguments = methodSymbol.ContainingType.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics.Concat(methodSymbol.TypeArgumentsWithAnnotations); + for (int i = 0; i < arity; i++) + { + var typeParameter = typeParameters[i]; + if (typeParameter.HasConstructorConstraint && + typeArguments[i].Type is NamedTypeSymbol typeArgument) + { + checkTypeArgumentWithConstructorConstraint(this, typeParameter, typeArgument, symbol, arg, location, diagnostics); + } + } + } + } + break; + + case NamedTypeSymbol typeSymbol: + { + var arity = typeSymbol.TypeParameters.Length; + for (int i = 0; i < arity; i++) + { + var typeParameter = typeSymbol.TypeParameters[i]; + if (typeParameter.HasConstructorConstraint && + typeSymbol.TypeArgumentsWithAnnotationsNoUseSiteDiagnostics[i].Type is NamedTypeSymbol typeArgument) + { + checkTypeArgumentWithConstructorConstraint(this, typeParameter, typeArgument, symbol, arg, location, diagnostics); + } + } + } + break; + } + } + + static void checkTypeArgumentWithConstructorConstraint(Binder @this, TypeParameterSymbol typeParameter, NamedTypeSymbol typeArgument, Symbol targetSymbol, T arg, Func location, DiagnosticBag diagnostics) + { + foreach (var ctor in typeArgument.InstanceConstructors) + { + if (ctor.ParameterCount == 0) + { + // An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + @this.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, ctor, arg, location, forConstructorConstraint: true, additionalArgs: [typeParameter, targetSymbol.OriginalDefinition]); + break; + } + } + } + } + + private void ReportDiagnosticsIfUnsafeMemberAccess(DiagnosticBag diagnostics, Symbol symbol, T arg, Func location, bool forConstructorConstraint, ReadOnlySpan additionalArgs = default) + { + var callerUnsafeMode = symbol.CallerUnsafeMode; + if (callerUnsafeMode != CallerUnsafeMode.None) + { + Debug.Assert(callerUnsafeMode == CallerUnsafeMode.Explicit || !forConstructorConstraint); + ReportUnsafeIfNotAllowed(arg, location, diagnostics, disallowedUnder: MemorySafetyRules.Updated, + customErrorCode: callerUnsafeMode switch + { + CallerUnsafeMode.Explicit => forConstructorConstraint ? ErrorCode.ERR_UnsafeConstructorConstraint : ErrorCode.ERR_UnsafeMemberOperation, + CallerUnsafeMode.Implicit => ErrorCode.ERR_UnsafeMemberOperationCompat, + _ => throw ExceptionUtilities.UnexpectedValue(callerUnsafeMode), + }, + customArgs: [symbol, .. additionalArgs]); + } + } + + /// + /// If this fails, call for the instead and add corresponding tests. + /// + [Conditional("DEBUG")] + internal void AssertNotUnsafeMemberAccess(Symbol symbol) + { + AssertNotUnsafeMemberAccess(symbol, ShouldCheckConstraints); + } + + /// + [Conditional("DEBUG")] + internal static void AssertNotUnsafeMemberAccess(Symbol symbol, bool shouldCheckConstraints = true) + { + // Resolving `symbol.ToString()` can lead to errors in some cases + // and `Debug.Assert` on .NET Framework evaluates all interpolated values eagerly, + // so avoid evaluating that unless we are going to fail anyway. + + if (symbol.CallerUnsafeMode is not CallerUnsafeMode.None) + { + Debug.Fail($"Symbol {symbol} has {nameof(symbol.CallerUnsafeMode)}={symbol.CallerUnsafeMode}."); + } + + if (symbol.Kind is SymbolKind.Method or SymbolKind.Property or SymbolKind.Event) + { + Debug.Fail($"Symbol {symbol} has {nameof(symbol.Kind)}={symbol.Kind}."); + } + + if (shouldCheckConstraints && symbol is NamedTypeSymbol { TypeParameters.Length: > 0 }) + { + Debug.Fail($"Symbol {symbol} is a generic type."); + } + } + + internal bool ReportUnsafeIfNotAllowed( + SyntaxNodeOrToken node, + BindingDiagnosticBag diagnostics, + MemorySafetyRules disallowedUnder, + TypeSymbol? sizeOfTypeOpt = null, + ErrorCode? customErrorCode = null, + object[]? customArgs = null) + { + return diagnostics.DiagnosticBag is { } bag && + ReportUnsafeIfNotAllowed(node, bag, disallowedUnder, sizeOfTypeOpt, customErrorCode, customArgs); + } + + internal bool ReportUnsafeIfNotAllowed( + SyntaxNodeOrToken node, + DiagnosticBag diagnostics, + MemorySafetyRules disallowedUnder, + TypeSymbol? sizeOfTypeOpt = null, + ErrorCode? customErrorCode = null, + object[]? customArgs = null) + { + Debug.Assert((node.Kind() == SyntaxKind.SizeOfExpression) == ((object?)sizeOfTypeOpt != null), "Should have a type for (only) sizeof expressions."); + return ReportUnsafeIfNotAllowed( + node, + static node => node.GetLocation(), + diagnostics, + disallowedUnder, + sizeOfTypeOpt, + customErrorCode, + customArgs); + } + + internal bool ReportUnsafeIfNotAllowed( + Location? location, + BindingDiagnosticBag diagnostics, + MemorySafetyRules disallowedUnder, + ErrorCode? customErrorCode = null, + object[]? customArgs = null) + { + return diagnostics.DiagnosticBag is { } bag && + ReportUnsafeIfNotAllowed(location, bag, disallowedUnder, customErrorCode, customArgs); + } + + internal bool ReportUnsafeIfNotAllowed( + Location? location, + DiagnosticBag diagnostics, + MemorySafetyRules disallowedUnder, + ErrorCode? customErrorCode = null, + object[]? customArgs = null) + { + return ReportUnsafeIfNotAllowed( + location, + static l => l, + diagnostics, + disallowedUnder, + sizeOfTypeOpt: null, + customErrorCode, + customArgs); + } + + /// + /// Memory safety rules which the current location is disallowed under. + /// /// True if a diagnostic was reported - internal bool ReportUnsafeIfNotAllowed(Location location, BindingDiagnosticBag diagnostics) + private bool ReportUnsafeIfNotAllowed( + T arg, + Func location, + DiagnosticBag diagnostics, + MemorySafetyRules disallowedUnder, + TypeSymbol? sizeOfTypeOpt = null, + ErrorCode? customErrorCode = null, + object[]? customArgs = null) { - var diagnosticInfo = GetUnsafeDiagnosticInfo(sizeOfTypeOpt: null); + var diagnosticInfo = GetUnsafeDiagnosticInfo(disallowedUnder, sizeOfTypeOpt, customErrorCode, customArgs); if (diagnosticInfo == null) { return false; } - diagnostics.Add(new CSDiagnostic(diagnosticInfo, location)); + diagnostics.Add(new CSDiagnostic(diagnosticInfo, location(arg))); return true; } - private CSDiagnosticInfo GetUnsafeDiagnosticInfo(TypeSymbol sizeOfTypeOpt) + private CSDiagnosticInfo? GetUnsafeDiagnosticInfo( + MemorySafetyRules disallowedUnder, + TypeSymbol? sizeOfTypeOpt, + ErrorCode? customErrorCode = null, + object[]? customArgs = null) { + Debug.Assert(sizeOfTypeOpt is null || disallowedUnder is MemorySafetyRules.Legacy); + if (this.Flags.Includes(BinderFlags.SuppressUnsafeDiagnostics)) { return null; } else if (!this.InUnsafeRegion) { - return ((object)sizeOfTypeOpt == null) - ? new CSDiagnosticInfo(ErrorCode.ERR_UnsafeNeeded) - : new CSDiagnosticInfo(ErrorCode.ERR_SizeofUnsafe, sizeOfTypeOpt); + if (disallowedUnder is MemorySafetyRules.Legacy) + { + Debug.Assert(customErrorCode is null && customArgs is null); + + if (this.Compilation.SourceModule.UseUpdatedMemorySafetyRules) + { + return MessageID.IDS_FeatureUnsafeEvolution.GetFeatureAvailabilityDiagnosticInfo(this.Compilation); + } + + return ((object?)sizeOfTypeOpt == null) + ? new CSDiagnosticInfo(ErrorCode.ERR_UnsafeNeeded) + : new CSDiagnosticInfo(ErrorCode.ERR_SizeofUnsafe, sizeOfTypeOpt); + } + + Debug.Assert(disallowedUnder is MemorySafetyRules.Updated); + + if (this.Compilation.SourceModule.UseUpdatedMemorySafetyRules) + { + return MessageID.IDS_FeatureUnsafeEvolution.GetFeatureAvailabilityDiagnosticInfo(this.Compilation) + ?? new CSDiagnosticInfo(customErrorCode ?? ErrorCode.ERR_UnsafeOperation, customArgs ?? []); + } + + // This location is disallowed only under updated memory safety rules which are not enabled. + // We report an error elsewhere, usually at the pointer type itself + // (where we are called with `disallowedUnder: MemorySafetyRules.Legacy`). + return null; } else if (this.IsIndirectlyInIterator && MessageID.IDS_FeatureRefUnsafeInIteratorAsync.GetFeatureAvailabilityDiagnosticInfo(Compilation) is { } unsafeInIteratorDiagnosticInfo) { - return unsafeInIteratorDiagnosticInfo; + if (disallowedUnder is MemorySafetyRules.Legacy) + { + return unsafeInIteratorDiagnosticInfo; + } + + // This location is disallowed only under updated memory safety rules. + // We report the RefUnsafeInIteratorAsync langversion error elsewhere, usually at the pointer type itself + // (where we are called with `disallowedUnder: MemorySafetyRules.Legacy`). + Debug.Assert(disallowedUnder is MemorySafetyRules.Updated); + return null; } else { @@ -72,4 +292,14 @@ private CSDiagnosticInfo GetUnsafeDiagnosticInfo(TypeSymbol sizeOfTypeOpt) } } } + + internal enum MemorySafetyRules + { + Legacy, + + /// + /// + /// + Updated, + } } diff --git a/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder.cs b/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder.cs index d3d3d381fce1..9a2040c617ae 100644 --- a/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder.cs +++ b/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder.cs @@ -11,7 +11,6 @@ using System.Text; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.CSharp.Symbols; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; @@ -65,6 +64,7 @@ internal sealed partial class DecisionDagBuilder /// affect code semantics but it results in a better code generation. /// private readonly bool _forLowering; + private bool _suitableForLowering = true; private DecisionDagBuilder(CSharpCompilation compilation, LabelSymbol defaultLabel, bool forLowering, BindingDiagnosticBag diagnostics) { @@ -115,25 +115,27 @@ public static BoundDecisionDag CreateDecisionDagForIsPattern( SyntaxNode syntax, BoundExpression inputExpression, BoundPattern pattern, + bool hasUnionMatching, LabelSymbol whenTrueLabel, LabelSymbol whenFalseLabel, BindingDiagnosticBag diagnostics, bool forLowering = false) { var builder = new DecisionDagBuilder(compilation, defaultLabel: whenFalseLabel, forLowering, diagnostics); - return builder.CreateDecisionDagForIsPattern(syntax, inputExpression, pattern, whenTrueLabel); + return builder.CreateDecisionDagForIsPattern(syntax, inputExpression, pattern, hasUnionMatching, whenTrueLabel); } private BoundDecisionDag CreateDecisionDagForIsPattern( SyntaxNode syntax, BoundExpression inputExpression, BoundPattern pattern, + bool hasUnionMatching, LabelSymbol whenTrueLabel) { var rootIdentifier = BoundDagTemp.ForOriginalInput(inputExpression); using var builder = TemporaryArray.Empty; - builder.Add(MakeTestsForPattern(index: 1, pattern.Syntax, rootIdentifier, pattern, whenClause: null, whenTrueLabel)); + builder.Add(MakeTestsForPattern(index: 1, pattern.Syntax, rootIdentifier, pattern, hasUnionMatching, whenClause: null, whenTrueLabel)); return MakeBoundDecisionDag(syntax, ref builder.AsRef()); } @@ -152,7 +154,7 @@ private BoundDecisionDag CreateDecisionDagForSwitchStatement( { if (label.Syntax.Kind() != SyntaxKind.DefaultSwitchLabel) { - builder.Add(MakeTestsForPattern(++i, label.Syntax, rootIdentifier, label.Pattern, label.WhenClause, label.Label)); + builder.Add(MakeTestsForPattern(++i, label.Syntax, rootIdentifier, label.Pattern, label.HasUnionMatching, label.WhenClause, label.Label)); } } } @@ -172,7 +174,7 @@ private BoundDecisionDag CreateDecisionDagForSwitchExpression( int i = 0; using var builder = TemporaryArray.GetInstance(switchArms.Length); foreach (BoundSwitchExpressionArm arm in switchArms) - builder.Add(MakeTestsForPattern(++i, arm.Syntax, rootIdentifier, arm.Pattern, arm.WhenClause, arm.Label)); + builder.Add(MakeTestsForPattern(++i, arm.Syntax, rootIdentifier, arm.Pattern, arm.HasUnionMatching, arm.WhenClause, arm.Label)); return MakeBoundDecisionDag(syntax, ref builder.AsRef()); } @@ -185,9 +187,15 @@ private StateForCase MakeTestsForPattern( SyntaxNode syntax, BoundDagTemp input, BoundPattern pattern, + bool hasUnionMatching, BoundExpression? whenClause, LabelSymbol label) { + if (hasUnionMatching) + { + pattern = UnionMatchingRewriter.Rewrite(_compilation, pattern); + } + Tests tests = MakeAndSimplifyTestsAndBindings(input, pattern, out ImmutableArray bindings); return new StateForCase(index, syntax, tests, bindings, whenClause, label); } @@ -316,7 +324,30 @@ private Tests MakeTestsAndBindings( BoundPattern pattern, ArrayBuilder bindings) { - return MakeTestsAndBindings(input, pattern, out _, bindings); + return MakeTestsAndBindings((TestInputOutputInfo)input, pattern, out _, bindings); + } + + private readonly struct TestInputOutputInfo(BoundDagTemp dagTemp, BoundPropertySubpatternMember? unionValue) + { + public readonly BoundDagTemp DagTemp = dagTemp; + public readonly BoundPropertySubpatternMember? UnionValue = unionValue; + + public static explicit operator TestInputOutputInfo(BoundDagTemp dagTemp) + { + return new TestInputOutputInfo(dagTemp, null); + } + + public static explicit operator BoundDagTemp(TestInputOutputInfo info) + { + if (info.UnionValue is not null) + { + throw ExceptionUtilities.Unreachable(); + } + + return info.DagTemp; + } + + public TypeSymbol GetInputType() => UnionValue?.Type ?? DagTemp.Type; } /// @@ -325,32 +356,49 @@ private Tests MakeTestsAndBindings( /// narrowed according to the pattern's *narrowed type*; see https://github.com/dotnet/csharplang/issues/2850. /// private Tests MakeTestsAndBindings( - BoundDagTemp input, + TestInputOutputInfo input, BoundPattern pattern, - out BoundDagTemp output, + out TestInputOutputInfo output, ArrayBuilder bindings) { - Debug.Assert(pattern.HasErrors || pattern.InputType.Equals(input.Type, TypeCompareKind.AllIgnoreOptions) || pattern.InputType.IsErrorType()); + Debug.Assert(!pattern.IsUnionMatching); + Debug.Assert(input.UnionValue is null ? + (pattern.HasErrors || pattern.InputType.Equals(input.DagTemp.Type, TypeCompareKind.AllIgnoreOptions) || pattern.InputType.IsErrorType()) : + (pattern.InputType.SpecialType == SpecialType.System_Object)); + switch (pattern) { case BoundDeclarationPattern declaration: - return MakeTestsAndBindingsForDeclarationPattern(input, declaration, out output, bindings); + { + return MakeTestsAndBindingsForDeclarationPattern(input, declaration, out output, bindings); + } case BoundConstantPattern constant: return MakeTestsForConstantPattern(input, constant, out output); case BoundDiscardPattern: case BoundSlicePattern: + Debug.Assert(input.UnionValue is null); output = input; return Tests.True.Instance; case BoundListPattern list: - return MakeTestsAndBindingsForListPattern(input, list, out output, bindings); + { + return MakeTestsAndBindingsForListPattern(input, list, out output, bindings); + } case BoundRecursivePattern recursive: - return MakeTestsAndBindingsForRecursivePattern(input, recursive, out output, bindings); + { + return MakeTestsAndBindingsForRecursivePattern(input, recursive, out output, bindings); + } case BoundITuplePattern iTuple: - return MakeTestsAndBindingsForITuplePattern(input, iTuple, out output, bindings); + { + return MakeTestsAndBindingsForITuplePattern(input, iTuple, out output, bindings); + } case BoundTypePattern type: - return MakeTestsForTypePattern(input, type, out output); + { + return MakeTestsForTypePattern(input, type, out output); + } case BoundRelationalPattern rel: - return MakeTestsAndBindingsForRelationalPattern(input, rel, out output); + { + return MakeTestsAndBindingsForRelationalPattern(input, rel, out output); + } case BoundNegatedPattern neg: output = input; return MakeTestsAndBindingsForNegatedPattern(input, neg, bindings); @@ -361,15 +409,92 @@ private Tests MakeTestsAndBindings( } } + private BoundDagTemp PrepareForUnionValuePropertyMatching(ref TestInputOutputInfo input, ArrayBuilder tests) + { + + if (input.UnionValue is { } unionValue) + { + BoundDagTemp temp = MakeUnionValue(input.DagTemp, unionValue, out BoundDagEvaluation valueEvaluation); + tests.Add(new Tests.One(valueEvaluation)); + input = (TestInputOutputInfo)temp; + } + + return (BoundDagTemp)input; + } + + private BoundDagTemp MakeUnionValue(BoundDagTemp input, BoundPropertySubpatternMember unionValue, out BoundDagEvaluation valueEvaluation) + { + Debug.Assert(unionValue.Symbol is PropertySymbol); + var property = (PropertySymbol)unionValue.Symbol; + valueEvaluation = new BoundDagPropertyEvaluation(unionValue.Syntax, property, isLengthOrCount: false, OriginalInput(input, property)); + var result = valueEvaluation.MakeResultTemp(); + + Debug.Assert(IsUnionValue(result, out _)); + return result; + } + + internal static bool IsUnionValue(BoundDagTemp input, [NotNullWhen(true)] out BoundDagTemp? unionInstance) + { + /* + input + └─dagTemp + ├─type: object + ├─source + │ └─dagPropertyEvaluation + │ ├─property: System.Runtime.CompilerServices.IUnion.Value + │ ├─isLengthOrCount: False + │ └─input + │ └─ < Union instance > + └─index: 0 + */ + if (input is + { + Index: 0, + Type.SpecialType: SpecialType.System_Object + } && + IsUnionValueEvaluation(input.Source, out unionInstance)) + { + return true; + } + + unionInstance = null; + return false; + } + + internal static bool IsUnionValueEvaluation(BoundDagEvaluation? source, [NotNullWhen(true)] out BoundDagTemp? unionInstance) + { + /* + source + └─dagPropertyEvaluation + ├─property: System.Runtime.CompilerServices.IUnion.Value + ├─isLengthOrCount: False + └─input + └─ < Union instance > + */ + if (source is BoundDagPropertyEvaluation + { + Property: { Name: WellKnownMemberNames.ValuePropertyName, Type.SpecialType: SpecialType.System_Object } property, + Input: { } propertyInput + } && + propertyInput.Type is NamedTypeSymbol { IsUnionType: true } match && + Binder.IsUnionTypeValueProperty(match, property)) + { + unionInstance = propertyInput; + return true; + } + + unionInstance = null; + return false; + } + private Tests MakeTestsAndBindingsForITuplePattern( - BoundDagTemp input, + TestInputOutputInfo inputInfo, BoundITuplePattern pattern, - out BoundDagTemp output, + out TestInputOutputInfo outputInfo, ArrayBuilder bindings) { var syntax = pattern.Syntax; var patternLength = pattern.Subpatterns.Length; - var objectType = this._compilation.GetSpecialType(SpecialType.System_Object); var getLengthProperty = (PropertySymbol)pattern.GetLengthMethod.AssociatedSymbol; RoslynDebug.Assert(getLengthProperty.Type.SpecialType == SpecialType.System_Int32); var getItemProperty = (PropertySymbol)pattern.GetItemMethod.AssociatedSymbol; @@ -377,15 +502,13 @@ private Tests MakeTestsAndBindingsForITuplePattern( RoslynDebug.Assert(iTupleType.Name == "ITuple"); var tests = ArrayBuilder.GetInstance(4 + patternLength * 2); - tests.Add(new Tests.One(new BoundDagTypeTest(syntax, iTupleType, input))); - var valueAsITupleEvaluation = new BoundDagTypeEvaluation(syntax, iTupleType, input); - tests.Add(new Tests.One(valueAsITupleEvaluation)); - var valueAsITuple = new BoundDagTemp(syntax, iTupleType, valueAsITupleEvaluation); - output = valueAsITuple; + inputInfo = MakeConvertToType(inputInfo, syntax, iTupleType, isExplicitTest: false, tests); + var valueAsITuple = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); + outputInfo = inputInfo; var lengthEvaluation = new BoundDagPropertyEvaluation(syntax, getLengthProperty, isLengthOrCount: true, OriginalInput(valueAsITuple, getLengthProperty)); tests.Add(new Tests.One(lengthEvaluation)); - var lengthTemp = new BoundDagTemp(syntax, this._compilation.GetSpecialType(SpecialType.System_Int32), lengthEvaluation); + var lengthTemp = lengthEvaluation.MakeResultTemp(); tests.Add(new Tests.One(new BoundDagValueTest(syntax, ConstantValue.Create(patternLength), lengthTemp))); var getItemPropertyInput = OriginalInput(valueAsITuple, getItemProperty); @@ -393,7 +516,7 @@ private Tests MakeTestsAndBindingsForITuplePattern( { var indexEvaluation = new BoundDagIndexEvaluation(syntax, getItemProperty, i, getItemPropertyInput); tests.Add(new Tests.One(indexEvaluation)); - var indexTemp = new BoundDagTemp(syntax, objectType, indexEvaluation); + var indexTemp = indexEvaluation.MakeResultTemp(); tests.Add(MakeTestsAndBindings(indexTemp, pattern.Subpatterns[i].Pattern, bindings)); } @@ -410,6 +533,14 @@ private BoundDagTemp OriginalInput(BoundDagTemp input, Symbol symbol) { while (input.Source is BoundDagTypeEvaluation source && isDerivedType(source.Input.Type, symbol.ContainingType)) { + if (IsAnyUnionValue(source.Input, out _)) + { + // Keep the type evaluation on top of a union try-get-value value parameter or Value property. + // This helps us unify the same value accessed through different Union APIs. + // See IsSameEntity/IsEqualEvaluation helpers. + break; + } + input = source.Input; } @@ -422,12 +553,21 @@ bool isDerivedType(TypeSymbol possibleDerived, TypeSymbol possibleBase) } } - private static BoundDagTemp OriginalInput(BoundDagTemp input) + internal static BoundDagTemp OriginalInput(BoundDagTemp input) { // Type evaluations do not change identity while (input.Source is BoundDagTypeEvaluation source) { Debug.Assert(input.Index == 0); + + if (IsAnyUnionValue(source.Input, out _)) + { + // Keep the type evaluation on top of a union try-get-value value parameter or Value property. + // This helps us unify the same value accessed through different Union APIs. + // See IsSameEntity/IsEqualEvaluation helpers. + break; + } + input = source.Input; } @@ -435,9 +575,9 @@ private static BoundDagTemp OriginalInput(BoundDagTemp input) } private Tests MakeTestsAndBindingsForDeclarationPattern( - BoundDagTemp input, + TestInputOutputInfo inputInfo, BoundDeclarationPattern declaration, - out BoundDagTemp output, + out TestInputOutputInfo outputInfo, ArrayBuilder bindings) { TypeSymbol? type = declaration.DeclaredType?.Type; @@ -445,11 +585,12 @@ private Tests MakeTestsAndBindingsForDeclarationPattern( // Add a null and type test if needed. if (!declaration.IsVar) - input = MakeConvertToType(input, declaration.Syntax, type!, isExplicitTest: false, tests); + inputInfo = MakeConvertToType(inputInfo, declaration.Syntax, type!, isExplicitTest: false, tests); BoundExpression? variableAccess = declaration.VariableAccess; if (variableAccess is { }) { + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); Debug.Assert(variableAccess.Type!.Equals(input.Type, TypeCompareKind.AllIgnoreOptions) || variableAccess.Type.IsErrorType()); bindings.Add(new BoundPatternBinding(variableAccess, input)); } @@ -458,18 +599,18 @@ private Tests MakeTestsAndBindingsForDeclarationPattern( RoslynDebug.Assert(declaration.Variable == null); } - output = input; + outputInfo = inputInfo; return Tests.AndSequence.Create(tests); } private Tests MakeTestsForTypePattern( - BoundDagTemp input, + TestInputOutputInfo input, BoundTypePattern typePattern, - out BoundDagTemp output) + out TestInputOutputInfo output) { TypeSymbol type = typePattern.DeclaredType.Type; var tests = ArrayBuilder.GetInstance(4); - output = MakeConvertToType(input: input, syntax: typePattern.Syntax, type: type, isExplicitTest: typePattern.IsExplicitNotNullTest, tests: tests); + output = MakeConvertToType(inputInfo: input, syntax: typePattern.Syntax, type: type, isExplicitTest: typePattern.IsExplicitNotNullTest, tests: tests); return Tests.AndSequence.Create(tests); } @@ -488,16 +629,201 @@ private static void MakeCheckNotNull( } } + private static bool IsUnionTryGetValueReturn(BoundDagTemp input, [NotNullWhen(true)] out TypeSymbol? targetType, [NotNullWhen(true)] out BoundDagTemp? unionInstance, [NotNullWhen(true)] out BoundDagDeconstructEvaluation? tryGetValueEvaluation) + { + /* + dagTemp + ├─type: bool + ├─source + │ └─dagDeconstructEvaluation + │ ├─deconstructMethod: S1.TryGetValue(out int) + │ └─input + │ └─ < Union instance > + └─index: -1 + */ + if (input.Index == -1 && + IsUnionTryGetValueEvaluation(input.Source, out targetType, out unionInstance)) + { + tryGetValueEvaluation = (BoundDagDeconstructEvaluation)input.Source; + return true; + } + + targetType = null; + unionInstance = null; + tryGetValueEvaluation = null; + return false; + } + + internal static bool IsUnionTryGetValueEvaluation([NotNullWhen(true)] BoundDagEvaluation? evaluation, [NotNullWhen(true)] out TypeSymbol? targetType, [NotNullWhen(true)] out BoundDagTemp? unionInstance) + { + /* + evaluation + └─dagDeconstructEvaluation + ├─deconstructMethod: S1.TryGetValue(out int) + └─input + └─ < Union instance > + */ + if (evaluation is BoundDagDeconstructEvaluation + { + DeconstructMethod: + { + Name: WellKnownMemberNames.TryGetValueMethodName, + ReturnType.SpecialType: SpecialType.System_Boolean, + DeclaredAccessibility: Accessibility.Public, + RefKind: RefKind.None, + Parameters: [{ RefKind: RefKind.Out, Type: var parameterType }], + }, + Input: { } tryGetValueInput + } && + tryGetValueInput.Type is NamedTypeSymbol { IsUnionType: true } match) + { + targetType = parameterType; + unionInstance = tryGetValueInput; + return true; + } + + targetType = null; + unionInstance = null; + return false; + } + + internal static bool IsUnionTryGetValueValue(BoundDagTemp input, [NotNullWhen(true)] out BoundDagTemp? unionInstance) + { + /* + dagTemp + ├─type: < Target type > + ├─source + │ └─dagDeconstructEvaluation + │ ├─deconstructMethod: S1.TryGetValue(out int) + │ └─input + │ └─ < Union instance > + └─index: 0 + */ + if (input.Index == 0 && + IsUnionTryGetValueEvaluation(input.Source, out _, out unionInstance)) + { + return true; + } + + unionInstance = null; + return false; + } + + private static bool IsUnionHasValue(BoundDagTemp input, [NotNullWhen(true)] out BoundDagTemp? unionInstance) + { + /* + dagExplicitNullTest + └─input + └─dagTemp + ├─type: object + ├─source + │ └─dagPropertyEvaluation + │ ├─property: S1.HasValue + │ ├─isLengthOrCount: False + │ └─input + │ └─ < Union instance > + └─index: 0 + */ + if (input is + { + Index: 0, + Type.SpecialType: SpecialType.System_Boolean, + Source: BoundDagPropertyEvaluation + { + Property: { Name: WellKnownMemberNames.HasValuePropertyName } property, + Input: { } propertyInput + } + } && + propertyInput.Type is NamedTypeSymbol { IsUnionType: true } match && + Binder.IsUnionTypeHasValueProperty(match, property)) + { + unionInstance = propertyInput; + return true; + } + + unionInstance = null; + return false; + } + + private bool TryMakeTestsForUnionHasValue(SyntaxNode syntax, TestInputOutputInfo inputInfo, bool sense, [NotNullWhen(true)] out Tests? tests) + { + if (inputInfo.UnionValue is { } unionValue && Binder.GetUnionTypeHasValueProperty((NamedTypeSymbol)inputInfo.DagTemp.Type) is PropertySymbol hasValue) + { + if (_forLowering) + { + BoundDagEvaluation hasValueEvaluation = new BoundDagPropertyEvaluation(unionValue.Syntax, hasValue, isLengthOrCount: false, OriginalInput(inputInfo.DagTemp, hasValue)); + var temp = hasValueEvaluation.MakeResultTemp(); + Debug.Assert(IsUnionHasValue(temp, out _)); + + Tests test = MakeConstantTest(syntax, temp, sense ? ConstantValue.True : ConstantValue.False); + Debug.Assert(test is Tests.One { Test: BoundDagValueTest }); + + tests = Tests.AndSequence.Create(new Tests.One(hasValueEvaluation), test); + return true; + } + else + { + _suitableForLowering = false; + } + } + + tests = null; + return false; + } + /// /// Generate a not-null check and a type check. /// - private BoundDagTemp MakeConvertToType( - BoundDagTemp input, + private TestInputOutputInfo MakeConvertToType( + TestInputOutputInfo inputInfo, SyntaxNode syntax, TypeSymbol type, bool isExplicitTest, ArrayBuilder tests) { + if (inputInfo.UnionValue is { } unionValue) + { + if (unionValue.Type.Equals(type, TypeCompareKind.AllIgnoreOptions) && + TryMakeTestsForUnionHasValue(syntax, inputInfo, sense: true, out var hasValueTests)) + { + tests.Add(hasValueTests); + return inputInfo; + } + + if (Binder.GetUnionTypeTryGetValueMethod((NamedTypeSymbol)inputInfo.DagTemp.Type, type) is MethodSymbol tryGetValue) + { + if (_forLowering) + { + var deconstructEvaluation = new BoundDagDeconstructEvaluation(syntax, tryGetValue, OriginalInput(inputInfo.DagTemp, tryGetValue)); + tests.Add(new Tests.One(deconstructEvaluation)); + + var boolResult = deconstructEvaluation.MakeReturnValueTemp(); + Debug.Assert(IsUnionTryGetValueReturn(boolResult, out _, out _, out _)); + + Tests test = MakeConstantTest(syntax, boolResult, ConstantValue.True); + Debug.Assert(test is Tests.One { Test: var tryGetValueResultTest } && IsUnionTryGetValueTest(tryGetValueResultTest, out _, out _, out _)); + + tests.Add(test); + + var outParameterTemp = deconstructEvaluation.MakeFirstOutParameterTemp(); + + // Add type evaluation after return value test to separate result value from deconstruct evaluation + // This helps us unify the same value accessed through different Union APIs. + // See IsSameEntity/IsEqualEvaluation helpers. + var typeEvaluation = new BoundDagTypeEvaluation(syntax, outParameterTemp.Type, outParameterTemp); + tests.Add(new Tests.One(typeEvaluation)); + + return (TestInputOutputInfo)typeEvaluation.MakeResultTemp(); + } + else + { + _suitableForLowering = false; + } + } + } + + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); + MakeCheckNotNull(input, syntax, isExplicitTest, tests); if (!input.Type.Equals(type, TypeCompareKind.AllIgnoreOptions)) { @@ -505,6 +831,7 @@ private BoundDagTemp MakeConvertToType( var useSiteInfo = new CompoundUseSiteInfo(_diagnostics, _compilation.Assembly); Conversion conversion = _conversions.ClassifyBuiltInConversion(inputType, type, isChecked: false, ref useSiteInfo); Debug.Assert(!conversion.IsUserDefined); + Debug.Assert(!conversion.IsUnion); _diagnostics.Add(syntax, useSiteInfo); if (input.Type.IsDynamic() ? type.SpecialType == SpecialType.System_Object : conversion.IsImplicit) @@ -518,75 +845,109 @@ private BoundDagTemp MakeConvertToType( } var evaluation = new BoundDagTypeEvaluation(syntax, type, input); - input = new BoundDagTemp(syntax, type, evaluation); + inputInfo = (TestInputOutputInfo)evaluation.MakeResultTemp(); tests.Add(new Tests.One(evaluation)); } - return input; + return inputInfo; } private Tests MakeTestsForConstantPattern( - BoundDagTemp input, + TestInputOutputInfo inputInfo, BoundConstantPattern constant, - out BoundDagTemp output) + out TestInputOutputInfo outputInfo) { if (constant.ConstantValue == ConstantValue.Null) { - output = input; - return new Tests.One(new BoundDagExplicitNullTest(constant.Syntax, input)); + if (TryMakeTestsForUnionHasValue(constant.Syntax, inputInfo, sense: false, out var tests)) + { + outputInfo = inputInfo; + return tests; + } + + var builder = ArrayBuilder.GetInstance(2); + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, builder); + outputInfo = inputInfo; + builder.Add(new Tests.One(new BoundDagExplicitNullTest(constant.Syntax, input))); + return Tests.AndSequence.Create(builder); } - else if (constant.ConstantValue.IsString && input.Type.IsSpanOrReadOnlySpanChar()) + else { - output = input; - return new Tests.One(new BoundDagValueTest(constant.Syntax, constant.ConstantValue, input)); + return makeTestsForNonNullConstantPattern(inputInfo, constant, out outputInfo); } - else + + Tests makeTestsForNonNullConstantPattern(TestInputOutputInfo inputInfo, BoundConstantPattern constant, out TestInputOutputInfo output) { - var tests = ArrayBuilder.GetInstance(2); - Debug.Assert(constant.Value.Type is not null || constant.HasErrors); - output = input = constant.Value.Type is { } type ? MakeConvertToType(input, constant.Syntax, type, isExplicitTest: false, tests) : input; - if (ValueSetFactory.ForInput(input)?.Related(BinaryOperatorKind.Equal, constant.ConstantValue).IsEmpty == true) + ConstantValue constantValue = constant.ConstantValue; + if (constantValue.IsString && (inputInfo.UnionValue?.Type ?? inputInfo.DagTemp.Type).IsSpanOrReadOnlySpanChar()) { - // This could only happen for a length input where the permitted value domain (>=0) is a strict subset of possible values for the type (int) - Debug.Assert(input.Source is BoundDagPropertyEvaluation { IsLengthOrCount: true }); - tests.Add(Tests.False.Instance); + var tests = ArrayBuilder.GetInstance(2); + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); + output = inputInfo; + tests.Add(new Tests.One(new BoundDagValueTest(constant.Syntax, constantValue, input))); + return Tests.AndSequence.Create(tests); } else { - tests.Add(new Tests.One(new BoundDagValueTest(constant.Syntax, constant.ConstantValue, input))); + var tests = ArrayBuilder.GetInstance(2); + Debug.Assert(constant.Value.Type is not null || constant.HasErrors); + if (constant.Value.Type is { } type) + { + inputInfo = MakeConvertToType(inputInfo, constant.Syntax, type, isExplicitTest: false, tests); + } + + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); + output = inputInfo; + tests.Add(MakeConstantTest(constant.Syntax, input, constantValue)); + return Tests.AndSequence.Create(tests); } - return Tests.AndSequence.Create(tests); + } + } + + private static Tests MakeConstantTest(SyntaxNode syntax, BoundDagTemp input, ConstantValue constantValue) + { + if (ValueSetFactory.ForInput(input)?.Related(BinaryOperatorKind.Equal, constantValue).IsEmpty == true) + { + // This could only happen for a length input where the permitted value domain (>=0) is a strict subset of possible values for the type (int) + Debug.Assert(input.Source is BoundDagPropertyEvaluation { IsLengthOrCount: true }); + return Tests.False.Instance; + } + else + { + return new Tests.One(new BoundDagValueTest(syntax, constantValue, input)); } } private Tests MakeTestsAndBindingsForRecursivePattern( - BoundDagTemp input, + TestInputOutputInfo inputInfo, BoundRecursivePattern recursive, - out BoundDagTemp output, + out TestInputOutputInfo outputInfo, ArrayBuilder bindings) { - RoslynDebug.Assert(input.Type.IsErrorType() || recursive.HasErrors || recursive.InputType.IsErrorType() || input.Type.Equals(recursive.InputType, TypeCompareKind.AllIgnoreOptions)); - var inputType = recursive.DeclaredType?.Type ?? input.Type.StrippedType(); + TypeSymbol inputTempType = inputInfo.GetInputType(); + RoslynDebug.Assert(inputTempType.IsErrorType() || recursive.HasErrors || recursive.InputType.IsErrorType() || inputTempType.Equals(recursive.InputType, TypeCompareKind.AllIgnoreOptions)); + var inputType = recursive.DeclaredType?.Type ?? inputTempType.StrippedType(); var tests = ArrayBuilder.GetInstance(5); - output = input = MakeConvertToType(input, recursive.Syntax, inputType, isExplicitTest: recursive.IsExplicitNotNullTest, tests); + inputInfo = MakeConvertToType(inputInfo, recursive.Syntax, inputType, isExplicitTest: recursive.IsExplicitNotNullTest, tests); if (!recursive.Deconstruction.IsDefault) { // we have a "deconstruction" form, which is either an invocation of a Deconstruct method, or a disassembly of a tuple if (recursive.DeconstructMethod != null) { + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); MethodSymbol method = recursive.DeconstructMethod; var evaluation = new BoundDagDeconstructEvaluation(recursive.Syntax, method, OriginalInput(input, method)); tests.Add(new Tests.One(evaluation)); - int extensionExtra = method.IsStatic ? 1 : 0; - int count = Math.Min(method.ParameterCount - extensionExtra, recursive.Deconstruction.Length); + + ArrayBuilder outParamTemps = evaluation.MakeOutParameterTemps(); + int count = Math.Min(outParamTemps.Count, recursive.Deconstruction.Length); for (int i = 0; i < count; i++) { BoundPattern pattern = recursive.Deconstruction[i].Pattern; - SyntaxNode syntax = pattern.Syntax; - var element = new BoundDagTemp(syntax, method.Parameters[i + extensionExtra].Type, evaluation, i); - tests.Add(MakeTestsAndBindings(element, pattern, bindings)); + tests.Add(MakeTestsAndBindings(outParamTemps[i], pattern, bindings)); } + outParamTemps.Free(); } else if (Binder.IsZeroElementTupleType(inputType)) { @@ -603,12 +964,13 @@ private Tests MakeTestsAndBindingsForRecursivePattern( int count = Math.Min(elementTypes.Length, recursive.Deconstruction.Length); for (int i = 0; i < count; i++) { + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); BoundPattern pattern = recursive.Deconstruction[i].Pattern; SyntaxNode syntax = pattern.Syntax; FieldSymbol field = elements[i]; var evaluation = new BoundDagFieldEvaluation(syntax, field, OriginalInput(input, field)); // fetch the ItemN field tests.Add(new Tests.One(evaluation)); - var element = new BoundDagTemp(syntax, field.Type, evaluation); + var element = evaluation.MakeResultTemp(); tests.Add(MakeTestsAndBindings(element, pattern, bindings)); } } @@ -617,6 +979,7 @@ private Tests MakeTestsAndBindingsForRecursivePattern( // This occurs in error cases. RoslynDebug.Assert(recursive.HasAnyErrors); // To prevent this pattern from subsuming other patterns and triggering a cascaded diagnostic, we add a test that will fail. + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); tests.Add(new Tests.One(new BoundDagTypeTest(recursive.Syntax, ErrorType(), input, hasErrors: true))); } } @@ -626,8 +989,24 @@ private Tests MakeTestsAndBindingsForRecursivePattern( // we have a "property" form foreach (var subpattern in recursive.Properties) { + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); BoundPattern pattern = subpattern.Pattern; BoundDagTemp currentInput = input; + + if (subpattern.Member is { Symbol: PropertySymbol { Name: WellKnownMemberNames.ValuePropertyName } property } && + input.Type is NamedTypeSymbol { IsUnionType: true } unionType && + Binder.IsUnionTypeValueProperty(unionType, property)) + { + // This sub-pattern is a union matching + + Debug.Assert(subpattern is { Member.Receiver: null, IsLengthOrCount: false }); // This is the shape created by UnionMatchingRewriter. + if (subpattern is { Member.Receiver: null, IsLengthOrCount: false }) + { + tests.Add(MakeTestsAndBindings(new TestInputOutputInfo(input, subpattern.Member), pattern, output: out _, bindings)); + continue; + } + } + if (!tryMakeTestsForSubpatternMember(subpattern.Member, ref currentInput, subpattern.IsLengthOrCount)) { Debug.Assert(recursive.HasAnyErrors); @@ -643,9 +1022,12 @@ private Tests MakeTestsAndBindingsForRecursivePattern( if (recursive.VariableAccess != null) { // we have a "variable" declaration + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); bindings.Add(new BoundPatternBinding(recursive.VariableAccess, input)); } + outputInfo = inputInfo; + return Tests.AndSequence.Create(tests); bool tryMakeTestsForSubpatternMember([NotNullWhen(true)] BoundPropertySubpatternMember? member, ref BoundDagTemp input, bool isLengthOrCount) @@ -657,38 +1039,45 @@ bool tryMakeTestsForSubpatternMember([NotNullWhen(true)] BoundPropertySubpattern if (tryMakeTestsForSubpatternMember(member.Receiver, ref input, isLengthOrCount: false)) { // If this is not the first member, add null test, unwrap nullables, and continue. - input = MakeConvertToType(input, member.Syntax, member.Receiver.Type.StrippedType(), isExplicitTest: false, tests); + input = (BoundDagTemp)MakeConvertToType((TestInputOutputInfo)input, member.Syntax, member.Receiver.Type.StrippedType(), isExplicitTest: false, tests); } BoundDagEvaluation evaluation; switch (member.Symbol) { case PropertySymbol property: - evaluation = new BoundDagPropertyEvaluation(member.Syntax, property, isLengthOrCount, OriginalInput(input, property)); - break; + { + var eval = new BoundDagPropertyEvaluation(member.Syntax, property, isLengthOrCount, OriginalInput(input, property)); + input = eval.MakeResultTemp(); + evaluation = eval; + break; + } case FieldSymbol field: - evaluation = new BoundDagFieldEvaluation(member.Syntax, field, OriginalInput(input, field)); - break; + { + var eval = new BoundDagFieldEvaluation(member.Syntax, field, OriginalInput(input, field)); + input = eval.MakeResultTemp(); + evaluation = eval; + break; + } default: return false; } tests.Add(new Tests.One(evaluation)); - input = new BoundDagTemp(member.Syntax, member.Type, evaluation); return true; } } - private Tests MakeTestsAndBindingsForNegatedPattern(BoundDagTemp input, BoundNegatedPattern neg, ArrayBuilder bindings) + private Tests MakeTestsAndBindingsForNegatedPattern(TestInputOutputInfo input, BoundNegatedPattern neg, ArrayBuilder bindings) { - var tests = MakeTestsAndBindings(input, neg.Negated, bindings); + var tests = MakeTestsAndBindings(input, neg.Negated, output: out _, bindings); return Tests.Not.Create(tests); } private Tests MakeTestsAndBindingsForBinaryPattern( - BoundDagTemp input, + TestInputOutputInfo inputInfo, BoundBinaryPattern bin, - out BoundDagTemp output, + out TestInputOutputInfo outputInfo, ArrayBuilder bindings) { // Users (such as ourselves) can have many, many nested binary patterns. To avoid crashing, do left recursion manually. @@ -703,36 +1092,39 @@ private Tests MakeTestsAndBindingsForBinaryPattern( } while (currentNode != null); currentNode = binaryPatternStack.Pop(); - Tests result = MakeTestsAndBindings(input, currentNode.Left, out output, bindings); + Tests result = MakeTestsAndBindings(inputInfo, currentNode.Left, out outputInfo, bindings); do { - result = makeTestsAndBindingsForBinaryPattern(this, result, output, input, currentNode, out output, bindings); + result = makeTestsAndBindingsForBinaryPattern(this, result, outputInfo, inputInfo, currentNode, out outputInfo, bindings); } while (binaryPatternStack.TryPop(out currentNode)); binaryPatternStack.Free(); return result; - static Tests makeTestsAndBindingsForBinaryPattern(DecisionDagBuilder @this, Tests leftTests, BoundDagTemp leftOutput, BoundDagTemp input, BoundBinaryPattern bin, out BoundDagTemp output, ArrayBuilder bindings) + Tests makeTestsAndBindingsForBinaryPattern(DecisionDagBuilder @this, Tests leftTests, TestInputOutputInfo leftOutputInfo, TestInputOutputInfo inputInfo, BoundBinaryPattern bin, out TestInputOutputInfo outputInfo, ArrayBuilder bindings) { var builder = ArrayBuilder.GetInstance(2); if (bin.Disjunction) { builder.Add(leftTests); - builder.Add(@this.MakeTestsAndBindings(input, bin.Right, bindings)); + builder.Add(@this.MakeTestsAndBindings(inputInfo, bin.Right, output: out _, bindings)); var result = Tests.OrSequence.Create(builder); if (bin.InputType.Equals(bin.NarrowedType)) { - output = input; + outputInfo = inputInfo; return result; } else { builder = ArrayBuilder.GetInstance(2); builder.Add(result); + + // https://github.com/dotnet/roslyn/issues/82636: Is there an advantage to use TryGetValue here? + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, builder); var evaluation = new BoundDagTypeEvaluation(bin.Syntax, bin.NarrowedType, input); - output = new BoundDagTemp(bin.Syntax, bin.NarrowedType, evaluation); + outputInfo = (TestInputOutputInfo)evaluation.MakeResultTemp(); builder.Add(new Tests.One(evaluation)); return Tests.AndSequence.Create(builder); } @@ -740,31 +1132,35 @@ static Tests makeTestsAndBindingsForBinaryPattern(DecisionDagBuilder @this, Test else { builder.Add(leftTests); - builder.Add(@this.MakeTestsAndBindings(leftOutput, bin.Right, out var rightOutput, bindings)); - output = rightOutput; - Debug.Assert(bin.HasErrors || output.Type.Equals(bin.NarrowedType, TypeCompareKind.AllIgnoreOptions)); + builder.Add(@this.MakeTestsAndBindings(leftOutputInfo, bin.Right, out var rightOutput, bindings)); + outputInfo = rightOutput; + Debug.Assert(bin.HasErrors || + (outputInfo.UnionValue is null ? + outputInfo.DagTemp.Type.Equals(bin.NarrowedType, TypeCompareKind.AllIgnoreOptions) : + bin.NarrowedType.SpecialType == SpecialType.System_Object)); return Tests.AndSequence.Create(builder); } } } private Tests MakeTestsAndBindingsForRelationalPattern( - BoundDagTemp input, + TestInputOutputInfo inputInfo, BoundRelationalPattern rel, - out BoundDagTemp output) + out TestInputOutputInfo outputInfo) { - var type = rel.Value.Type ?? input.Type; + var type = rel.Value.Type ?? inputInfo.GetInputType(); Debug.Assert(type is { }); // check if the test is always true or always false var tests = ArrayBuilder.GetInstance(2); - output = MakeConvertToType(input, rel.Syntax, type, isExplicitTest: false, tests); + outputInfo = MakeConvertToType(inputInfo, rel.Syntax, type, isExplicitTest: false, tests); + BoundDagTemp output = PrepareForUnionValuePropertyMatching(ref outputInfo, tests); var fac = ValueSetFactory.ForInput(output); - var values = fac?.Related(rel.Relation.Operator(), rel.ConstantValue); + IConstantValueSet? values = fac?.Related(rel.Relation.Operator(), rel.ConstantValue); if (values?.IsEmpty == true) { tests.Add(Tests.False.Instance); } - else if (values?.Complement().IsEmpty != true) + else if (((IConstantValueSet?)values?.Complement())?.IsEmpty != true) { tests.Add(new Tests.One(new BoundDagRelationalTest(rel.Syntax, rel.Relation, rel.ConstantValue, output, rel.HasErrors))); } @@ -801,7 +1197,8 @@ private BoundDecisionDag MakeBoundDecisionDag(SyntaxNode syntax, ref TemporaryAr var rootDecisionDagNode = decisionDag.RootNode.Dag; RoslynDebug.Assert(rootDecisionDagNode != null); - var boundDecisionDag = new BoundDecisionDag(rootDecisionDagNode.Syntax, rootDecisionDagNode); + Debug.Assert(_suitableForLowering || !_forLowering); + var boundDecisionDag = new BoundDecisionDag(rootDecisionDagNode.Syntax, rootDecisionDagNode, _suitableForLowering); // Now go and clean up all the dag states we created foreach (var kvp in uniqueState) @@ -973,23 +1370,10 @@ DagState uniquifyState(FrozenArrayBuilder cases, ImmutableDictiona else { // Select the next test to do at this state, and compute successor states - switch (state.SelectedTest = state.ComputeSelectedTest()) - { - case BoundDagAssignmentEvaluation e when state.RemainingValues.TryGetValue(e.Input, out IValueSet? currentValues): - Debug.Assert(e.Input.IsEquivalentTo(e.Target)); - // Update the target temp entry with current values. Note that even though we have determined that the two are the same, - // we don't need to update values for the current input. We will emit another assignment node with this temp as the target - // if apropos, which has the effect of flowing the remaining values from the other test in the analysis of subsequent states. - if (state.RemainingValues.TryGetValue(e.Target, out IValueSet? targetValues)) - { - // Take the intersection of entries as we have ruled out any impossible - // values for each alias of an element, and now we're dealiasing them. - currentValues = currentValues.Intersect(targetValues); - } - state.TrueBranch = uniquifyState(RemoveEvaluation(state.Cases, e), state.RemainingValues.SetItem(e.Target, currentValues)); - break; + switch (state.SelectedTest = state.ComputeSelectedTest(_forLowering, ref _suitableForLowering)) + { case BoundDagEvaluation e: - state.TrueBranch = uniquifyState(RemoveEvaluation(state.Cases, e), state.RemainingValues); + state.TrueBranch = uniquifyState(RemoveEvaluation(state, e), state.RemainingValues); // An evaluation is considered to always succeed, so there is no false branch break; case BoundDagTest d: @@ -1012,71 +1396,240 @@ DagState uniquifyState(FrozenArrayBuilder cases, ImmutableDictiona } } - return new DecisionDag(initialState); - } - - /// - /// Compute the corresponding to each of the given - /// and store it in . - /// - private void ComputeBoundDecisionDagNodes(DecisionDag decisionDag, BoundLeafDecisionDagNode defaultDecision) - { - Debug.Assert(_defaultLabel != null); - Debug.Assert(defaultDecision != null); + return removeUnnecessaryStates(initialState, new DecisionDag(initialState)); - // Process the states in topological order, leaves first, and assign a BoundDecisionDag to each DagState. - bool wasAcyclic = decisionDag.TryGetTopologicallySortedReachableStates(out ImmutableArray sortedStates); - if (!wasAcyclic) + static DecisionDag removeUnnecessaryStates(DagState initialState, DecisionDag result) { - // Since we intend the set of DagState nodes to be acyclic by construction, we do not expect - // this to occur. Just in case it does due to bugs, we recover gracefully to avoid crashing the - // compiler in production. If you find that this happens (the assert fails), please modify the - // DagState construction process to avoid creating a cyclic state graph. - Debug.Assert(wasAcyclic, "wasAcyclic"); // force failure in debug builds + // There still might be states with evaluations that produce results that aren't used, or tests + // that make no difference (i.e. the outcome is the same whether the result is true or false) + // Let's check for these cases and optimize the Dag - // If the dag contains a cycle, return a short-circuit dag instead. - decisionDag.RootNode.Dag = defaultDecision; - return; - } + if (result.TryGetTopologicallySortedReachableStates(out ImmutableArray states)) + { + var tempToIndex = PooledDictionary.GetInstance(); + int nextTempIndex = 0; + var stateToIndex = PooledDictionary.GetInstance(); + var usedTempsPerState = ArrayBuilder.GetInstance(states.Length, BitVector.Empty); + BitVector unnecessaryStates = BitVector.Empty; + + bool changedStates = false; + for (int i = states.Length - 1; i >= 0; i--) + { + DagState state = states[i]; - // We "intern" the dag nodes, so that we only have a single object representing one - // semantic node. We do this because different states may end up mapping to the same - // set of successor states. In this case we merge them when producing the bound state machine. - var uniqueNodes = PooledDictionary.GetInstance(); - BoundDecisionDagNode uniqifyDagNode(BoundDecisionDagNode node) => uniqueNodes.GetOrAdd(node, node); + DagState? trueBranch = state.TrueBranch; + DagState? falseBranch = state.FalseBranch; - _ = uniqifyDagNode(defaultDecision); + var usedTemps = BitVector.Empty; - for (int i = sortedStates.Length - 1; i >= 0; i--) - { - var state = sortedStates[i]; - if (state.Cases.Count == 0) - { - state.Dag = defaultDecision; - continue; - } + if (state.Cases is [{ PatternIsSatisfied: true } stateForCase, ..]) + { + Debug.Assert(state.SelectedTest is null); + markTempsUsedInBindings(tempToIndex, ref nextTempIndex, ref usedTemps, stateForCase); + } - StateForCase first = state.Cases[0]; - RoslynDebug.Assert(!(first.RemainingTests is Tests.False)); - if (first.PatternIsSatisfied) - { - if (first.IsFullyMatched) - { - // there is no when clause we need to evaluate - state.Dag = finalState(first.Syntax, first.CaseLabel, first.Bindings); - } - else - { - RoslynDebug.Assert(state.TrueBranch == null); - RoslynDebug.Assert(state.FalseBranch is { }); + if (trueBranch != null) + { + usedTemps.UnionWith(usedTempsPerState[stateToIndex[trueBranch]]); + } - // The final state here does not need bindings, as they will be performed before evaluating the when clause (see below) - BoundDecisionDagNode whenTrue = finalState(first.Syntax, first.CaseLabel, default); - BoundDecisionDagNode? whenFalse = state.FalseBranch.Dag; - RoslynDebug.Assert(whenFalse is { }); - // Note: we may share `when` clauses between multiple DAG nodes, but we deal with that safely during lowering - state.Dag = uniqifyDagNode(new BoundWhenDecisionDagNode(first.Syntax, first.Bindings, first.WhenClause, whenTrue, whenFalse)); - } + if (falseBranch != null) + { + usedTemps.UnionWith(usedTempsPerState[stateToIndex[falseBranch]]); + } + + if (trueBranch != null && unnecessaryStates[stateToIndex[trueBranch]]) + { + state.TrueBranch = trueBranch = trueBranch.TrueBranch; + changedStates = true; + } + + if (falseBranch != null && unnecessaryStates[stateToIndex[falseBranch]]) + { + state.FalseBranch = falseBranch = falseBranch.TrueBranch; + changedStates = true; + } + + if (state.SelectedTest is BoundDagEvaluation eval) + { + Debug.Assert(state.FalseBranch is null); + Debug.Assert(trueBranch is not null); + Debug.Assert(state.TrueBranch is not null); + + // Cases in the TrueBranch state might drop some of the cases, but maintain the same relative order for remaining cases + for (int case1 = 0, case2 = 0; case1 < state.Cases.Count; case1++) + { + StateForCase stateForCases = state.Cases[case1]; + + if (case2 < state.TrueBranch.Cases.Count && stateForCases.CaseLabel == state.TrueBranch.Cases[case2].CaseLabel) + { + // Found matching case + Debug.Assert(stateForCases.Index == state.TrueBranch.Cases[case2].Index); + case2++; + } + else + { + Debug.Assert(case2 >= state.TrueBranch.Cases.Count || stateForCases.Index != state.TrueBranch.Cases[case2].Index); + + // This is to handle the situation when RemoveEvaluation drops the case the evaluation came from + markTempsUsedInBindings(tempToIndex, ref nextTempIndex, ref usedTemps, stateForCases); + } + } + + if (eval is BoundDagAssignmentEvaluation) + { + // An assignment evaluation is always meaningful + markUsedTemp(tempToIndex, ref nextTempIndex, ref usedTemps, eval.Input); + } + else + { + OneOrMany outputs = eval.AllOutputs(); + bool anyOutputUsed = false; + + foreach (var temp in outputs) + { + if (tempToIndex.TryGetValue(temp, out int index) && usedTemps[index]) + { + anyOutputUsed = true; + break; + } + } + + if (anyOutputUsed) + { + markAllInputsUsed(tempToIndex, ref nextTempIndex, ref usedTemps, eval); + } + else + { + // Evaluation is not necessary + unnecessaryStates[i] = true; + } + } + } + else if (state.SelectedTest is BoundDagTest test) + { + Debug.Assert(trueBranch is not null); + Debug.Assert(falseBranch is not null); + + if (trueBranch == falseBranch) + { + // No need to pass through this state and do the test + unnecessaryStates[i] = true; + } + else + { + markAllInputsUsed(tempToIndex, ref nextTempIndex, ref usedTemps, test); + } + } + + stateToIndex.Add(state, i); + usedTempsPerState[i] = usedTemps; + } + + stateToIndex.Free(); + usedTempsPerState.Free(); + tempToIndex.Free(); + + if (changedStates) + { + result = new DecisionDag(initialState); + } + } + + return result; + } + + static void markTempsUsedInBindings(PooledDictionary tempToIndex, ref int nextTempIndex, ref BitVector usedTemps, StateForCase stateForCase) + { + foreach (var b in stateForCase.Bindings) + { + markUsedTemp(tempToIndex, ref nextTempIndex, ref usedTemps, b.TempContainingValue); + } + } + + static void markUsedTemp(PooledDictionary tempToIndex, ref int nextTempIndex, ref BitVector usedTemps, BoundDagTemp temp) + { + int tempIndex = tempToIndex.GetOrAdd(temp, nextTempIndex); + + if (tempIndex == nextTempIndex) + { + nextTempIndex++; + } + + usedTemps[tempIndex] = true; + } + + static void markAllInputsUsed(PooledDictionary tempToIndex, ref int nextTempIndex, ref BitVector usedTemps, BoundDagTest test) + { + foreach (var temp in test.AllInputs()) + { + markUsedTemp(tempToIndex, ref nextTempIndex, ref usedTemps, temp); + } + } + } + + /// + /// Compute the corresponding to each of the given + /// and store it in . + /// + private void ComputeBoundDecisionDagNodes(DecisionDag decisionDag, BoundLeafDecisionDagNode defaultDecision) + { + Debug.Assert(_defaultLabel != null); + Debug.Assert(defaultDecision != null); + + // Process the states in topological order, leaves first, and assign a BoundDecisionDag to each DagState. + bool wasAcyclic = decisionDag.TryGetTopologicallySortedReachableStates(out ImmutableArray sortedStates); + if (!wasAcyclic) + { + // Since we intend the set of DagState nodes to be acyclic by construction, we do not expect + // this to occur. Just in case it does due to bugs, we recover gracefully to avoid crashing the + // compiler in production. If you find that this happens (the assert fails), please modify the + // DagState construction process to avoid creating a cyclic state graph. + Debug.Assert(wasAcyclic, "wasAcyclic"); // force failure in debug builds + + // If the dag contains a cycle, return a short-circuit dag instead. + decisionDag.RootNode.Dag = defaultDecision; + return; + } + + // We "intern" the dag nodes, so that we only have a single object representing one + // semantic node. We do this because different states may end up mapping to the same + // set of successor states. In this case we merge them when producing the bound state machine. + var uniqueNodes = PooledDictionary.GetInstance(); + BoundDecisionDagNode uniqifyDagNode(BoundDecisionDagNode node) => uniqueNodes.GetOrAdd(node, node); + + _ = uniqifyDagNode(defaultDecision); + + for (int i = sortedStates.Length - 1; i >= 0; i--) + { + var state = sortedStates[i]; + if (state.Cases.Count == 0) + { + state.Dag = defaultDecision; + continue; + } + + StateForCase first = state.Cases[0]; + RoslynDebug.Assert(!(first.RemainingTests is Tests.False)); + if (first.PatternIsSatisfied) + { + if (first.IsFullyMatched) + { + // there is no when clause we need to evaluate + state.Dag = finalState(first.Syntax, first.CaseLabel, first.Bindings); + } + else + { + RoslynDebug.Assert(state.TrueBranch == null); + RoslynDebug.Assert(state.FalseBranch is { }); + + // The final state here does not need bindings, as they will be performed before evaluating the when clause (see below) + BoundDecisionDagNode whenTrue = finalState(first.Syntax, first.CaseLabel, default); + BoundDecisionDagNode? whenFalse = state.FalseBranch.Dag; + RoslynDebug.Assert(whenFalse is { }); + // Note: we may share `when` clauses between multiple DAG nodes, but we deal with that safely during lowering + state.Dag = uniqifyDagNode(new BoundWhenDecisionDagNode(first.Syntax, first.Bindings, first.WhenClause, whenTrue, whenFalse)); + } BoundDecisionDagNode finalState(SyntaxNode syntax, LabelSymbol label, ImmutableArray bindings) { @@ -1169,7 +1722,7 @@ private void SplitCases( whenFalse = AsFrozen(whenFalseBuilder); } - private static ( + private ( ImmutableDictionary whenTrueValues, ImmutableDictionary whenFalseValues, bool truePossible, @@ -1181,10 +1734,13 @@ private static ( switch (test) { case BoundDagEvaluation _: - case BoundDagExplicitNullTest _: - case BoundDagNonNullTest _: - case BoundDagTypeTest _: return (values, values, true, true); + case BoundDagExplicitNullTest nullTest: + return resultForNullTest(nullTest); + case BoundDagNonNullTest nonNullTest: + return resultForNonNullTest(nonNullTest); + case BoundDagTypeTest typeTest: + return resultForTypeTest(typeTest); case BoundDagValueTest t: return resultForRelation(BinaryOperatorKind.Equal, t.Value); case BoundDagRelationalTest t: @@ -1201,22 +1757,101 @@ private static ( resultForRelation(BinaryOperatorKind relation, ConstantValue value) { var input = test.Input; - IValueSetFactory? valueFac = ValueSetFactory.ForInput(input); + IConstantValueSetFactory? valueFac = ValueSetFactory.ForInput(input); if (valueFac == null || value.IsBad) { // If it is a type we don't track yet, assume all values are possible return (values, values, true, true); } - IValueSet fromTestPassing = valueFac.Related(relation.Operator(), value); - IValueSet fromTestFailing = fromTestPassing.Complement(); + var fromTestPassing = valueFac.Related(relation.Operator(), value); + (var whenTrueValues, var whenFalseValues, fromTestPassing, var fromTestFailing) = splitValues(values, input, fromTestPassing); + return (whenTrueValues, whenFalseValues, !fromTestPassing.IsEmpty, !fromTestFailing.IsEmpty); + } + + static ( + ImmutableDictionary whenTrueValues, + ImmutableDictionary whenFalseValues, + TValueSet fromTestPassing, + TValueSet fromTestFailing) + splitValues(ImmutableDictionary values, BoundDagTemp input, TValueSet fromTestPassing) where TValueSet : IValueSet + { + var fromTestFailing = (TValueSet)fromTestPassing.Complement(); if (values.TryGetValue(input, out IValueSet? tempValuesBeforeTest)) { - fromTestPassing = fromTestPassing.Intersect(tempValuesBeforeTest); - fromTestFailing = fromTestFailing.Intersect(tempValuesBeforeTest); + fromTestPassing = (TValueSet)fromTestPassing.Intersect(tempValuesBeforeTest); + fromTestFailing = (TValueSet)fromTestFailing.Intersect(tempValuesBeforeTest); } var whenTrueValues = values.SetItem(input, fromTestPassing); var whenFalseValues = values.SetItem(input, fromTestFailing); - return (whenTrueValues, whenFalseValues, !fromTestPassing.IsEmpty, !fromTestFailing.IsEmpty); + return (whenTrueValues, whenFalseValues, fromTestPassing, fromTestFailing); + } + + ( + ImmutableDictionary whenTrueValues, + ImmutableDictionary whenFalseValues, + bool truePossible, + bool falsePossible) + resultForTypeTest(BoundDagTypeTest typeTest) + { + if (!_forLowering && ValueSetFactory.TypeUnionValueSetFactoryForInput(typeTest.Input) is { } factory) + { + var useSiteInfo = new CompoundUseSiteInfo(_diagnostics, _compilation.Assembly); + var fromTestPassing = factory.FromTypeMatch(typeTest.Type, _conversions, ref useSiteInfo); + if (!fromTestPassing.IsEmpty(ref useSiteInfo)) // Otherwise we must have reported ERR_PatternWrongType during binding, let's avoid cascading errors + { + (var whenTrueValues, var whenFalseValues, fromTestPassing, var fromTestFailing) = splitValues(values, typeTest.Input, fromTestPassing); + var result = (whenTrueValues, whenFalseValues, !fromTestPassing.IsEmpty(ref useSiteInfo), !fromTestFailing.IsEmpty(ref useSiteInfo)); + _diagnostics.Add(typeTest.Syntax, useSiteInfo); + _suitableForLowering = false; + return result; + } + + _diagnostics.Add(typeTest.Syntax, useSiteInfo); + } + + return (values, values, true, true); + } + + ( + ImmutableDictionary whenTrueValues, + ImmutableDictionary whenFalseValues, + bool truePossible, + bool falsePossible) + resultForNonNullTest(BoundDagNonNullTest nonNullTest) + { + if (!_forLowering && ValueSetFactory.TypeUnionValueSetFactoryForInput(nonNullTest.Input) is { } factory) + { + var fromTestPassing = factory.FromNonNullMatch(_conversions); + var useSiteInfo = new CompoundUseSiteInfo(_diagnostics, _compilation.Assembly); + (var whenTrueValues, var whenFalseValues, fromTestPassing, var fromTestFailing) = splitValues(values, nonNullTest.Input, fromTestPassing); + var result = (whenTrueValues, whenFalseValues, !fromTestPassing.IsEmpty(ref useSiteInfo), !fromTestFailing.IsEmpty(ref useSiteInfo)); + _diagnostics.Add(nonNullTest.Syntax, useSiteInfo); + _suitableForLowering = false; + return result; + } + + return (values, values, true, true); + } + + ( + ImmutableDictionary whenTrueValues, + ImmutableDictionary whenFalseValues, + bool truePossible, + bool falsePossible) + resultForNullTest(BoundDagExplicitNullTest nullTest) + { + if (!_forLowering && ValueSetFactory.TypeUnionValueSetFactoryForInput(nullTest.Input) is { } factory) + { + var fromTestPassing = factory.FromNullMatch(_conversions); + var useSiteInfo = new CompoundUseSiteInfo(_diagnostics, _compilation.Assembly); + (var whenTrueValues, var whenFalseValues, fromTestPassing, var fromTestFailing) = splitValues(values, nullTest.Input, fromTestPassing); + var result = (whenTrueValues, whenFalseValues, !fromTestPassing.IsEmpty(ref useSiteInfo), !fromTestFailing.IsEmpty(ref useSiteInfo)); + _diagnostics.Add(nullTest.Syntax, useSiteInfo); + _suitableForLowering = false; + return result; + } + + return (values, values, true, true); } } @@ -1236,7 +1871,7 @@ private static (BoundDagTemp? lengthTemp, int offset) TryGetTopLevelLengthTemp(B return (lengthTemp, offset); } - private static (BoundDagTemp input, BoundDagTemp lengthTemp, int index) GetCanonicalInput(BoundDagIndexerEvaluation e) + internal static (BoundDagTemp input, BoundDagTemp lengthTemp, int index) GetCanonicalInput(BoundDagIndexerEvaluation e) { int index = e.Index; BoundDagTemp input = e.Input; @@ -1248,15 +1883,16 @@ private static (BoundDagTemp input, BoundDagTemp lengthTemp, int index) GetCanon lengthTemp = slice.LengthTemp; input = slice.Input; } - return (OriginalInput(input), lengthTemp, index); + return (input, lengthTemp, index); } - private static FrozenArrayBuilder RemoveEvaluation(FrozenArrayBuilder cases, BoundDagEvaluation e) + private FrozenArrayBuilder RemoveEvaluation(DagState state, BoundDagEvaluation e) { + FrozenArrayBuilder cases = state.Cases; var builder = ArrayBuilder.GetInstance(cases.Count); foreach (var stateForCase in cases) { - var remainingTests = stateForCase.RemainingTests.RemoveEvaluation(e); + var remainingTests = stateForCase.RemainingTests.RemoveEvaluation(this, state, stateForCase.Bindings, e); if (remainingTests is Tests.False) { // This can occur in error cases like `e is not int x` where there is a trailing evaluation @@ -1304,6 +1940,7 @@ private void CheckConsistentDecision( trueTestImpliesTrueOther = false; falseTestImpliesTrueOther = false; + // The logic used here around cross matching type and null tests is duplicated in CheckConsistentUnionDecision. switch (test) { case BoundDagNonNullTest _: @@ -1328,6 +1965,18 @@ private void CheckConsistentDecision( // !(v != null) --> !(v != null) falseTestPermitsTrueOther = false; break; + case BoundDagTypeTest t2 when !_forLowering: + if (whenTrueValues is TypeUnionValueSet whenTrueUnionSet) + { + var useSiteInfo = new CompoundUseSiteInfo(_diagnostics, _compilation.Assembly); + if (whenTrueUnionSet.TypeMatchesAllValuesIfAny(t2.Type, ref useSiteInfo)) + { + trueTestImpliesTrueOther = true; + _suitableForLowering = false; + } + } + + goto default; default: // !(v != null) --> !(v is T) falseTestPermitsTrueOther = false; @@ -1344,37 +1993,14 @@ private void CheckConsistentDecision( trueTestImpliesTrueOther = true; break; case BoundDagTypeTest t2: - { - var useSiteInfo = new CompoundUseSiteInfo(_diagnostics, _compilation.Assembly); - ConstantValue? matches = ExpressionOfTypeMatchesPatternTypeForLearningFromSuccessfulTypeTest(t1.Type, t2.Type, ref useSiteInfo); - if (matches == ConstantValue.False) - { - // If T1 could never be T2 - // v is T1 --> !(v is T2) - trueTestPermitsTrueOther = false; - } - else if (matches == ConstantValue.True) - { - // If T1: T2 - // v is T1 --> v is T2 - trueTestImpliesTrueOther = true; - } - - // If every T2 is a T1, then failure of T1 implies failure of T2. - matches = Binder.ExpressionOfTypeMatchesPatternType(_conversions, t2.Type, t1.Type, ref useSiteInfo, out _); - _diagnostics.Add(syntax, useSiteInfo); - if (matches == ConstantValue.True) - { - // If T2: T1 - // !(v is T1) --> !(v is T2) - falseTestPermitsTrueOther = false; - } - } + CheckConsistentTypeTestsDecision(whenFalseValues, syntax, ref trueTestPermitsTrueOther, ref falseTestPermitsTrueOther, ref trueTestImpliesTrueOther, ref falseTestImpliesTrueOther, t1.Type, t2.Type); break; case BoundDagExplicitNullTest _: - foundExplicitNullTest = true; - // v is T --> !(v == null) - trueTestPermitsTrueOther = false; + { + foundExplicitNullTest = true; + // v is T --> !(v == null) + trueTestPermitsTrueOther = false; + } break; } break; @@ -1402,69 +2028,354 @@ private void CheckConsistentDecision( out trueTestPermitsTrueOther, out falseTestPermitsTrueOther, out trueTestImpliesTrueOther, out falseTestImpliesTrueOther); break; - void handleRelationWithValue( - BinaryOperatorKind relation, - ConstantValue value, - out bool trueTestPermitsTrueOther, - out bool falseTestPermitsTrueOther, - out bool trueTestImpliesTrueOther, - out bool falseTestImpliesTrueOther) - { - // We check test.Equals(other) to handle "bad" constant values - bool sameTest = test.Equals(other); - trueTestPermitsTrueOther = whenTrueValues?.Any(relation, value) ?? true; - trueTestImpliesTrueOther = sameTest || trueTestPermitsTrueOther && (whenTrueValues?.All(relation, value) ?? false); - falseTestPermitsTrueOther = !sameTest && (whenFalseValues?.Any(relation, value) ?? true); - falseTestImpliesTrueOther = falseTestPermitsTrueOther && (whenFalseValues?.All(relation, value) ?? false); - } - } - break; - case BoundDagExplicitNullTest _: - foundExplicitNullTest = true; - switch (other) + void handleRelationWithValue( + BinaryOperatorKind relation, + ConstantValue value, + out bool trueTestPermitsTrueOther, + out bool falseTestPermitsTrueOther, + out bool trueTestImpliesTrueOther, + out bool falseTestImpliesTrueOther) + { + // We check test.Equals(other) to handle "bad" constant values + bool sameTest = test.Equals(other); + var whenTrueConstantValueSet = whenTrueValues as IConstantValueSet; + trueTestPermitsTrueOther = whenTrueConstantValueSet?.Any(relation, value) ?? true; + trueTestImpliesTrueOther = sameTest || trueTestPermitsTrueOther && (whenTrueConstantValueSet?.All(relation, value) ?? false); + var whenFalseConstantValueSet = whenFalseValues as IConstantValueSet; + falseTestPermitsTrueOther = !sameTest && (whenFalseConstantValueSet?.Any(relation, value) ?? true); + falseTestImpliesTrueOther = falseTestPermitsTrueOther && (whenFalseConstantValueSet?.All(relation, value) ?? false); + } + } + break; + case BoundDagExplicitNullTest _: + foundExplicitNullTest = true; + switch (other) + { + case BoundDagNonNullTest n2: + if (n2.IsExplicitTest) + foundExplicitNullTest = true; + // v == null --> !(v != null) + trueTestPermitsTrueOther = false; + // !(v == null) --> v != null + falseTestImpliesTrueOther = true; + break; + case BoundDagTypeTest t2: + if (!_forLowering && whenFalseValues is TypeUnionValueSet whenFalseUnionSet) + { + var useSiteInfo = new CompoundUseSiteInfo(_diagnostics, _compilation.Assembly); + if (whenFalseUnionSet.TypeMatchesAllValuesIfAny(t2.Type, ref useSiteInfo)) + { + falseTestImpliesTrueOther = true; + _suitableForLowering = false; + } + } + + // v == null --> !(v is T) + trueTestPermitsTrueOther = false; + break; + case BoundDagExplicitNullTest _: + foundExplicitNullTest = true; + // v == null --> v == null + trueTestImpliesTrueOther = true; + // !(v == null) --> !(v == null) + falseTestPermitsTrueOther = false; + break; + case BoundDagValueTest _: + // v == null --> !(v == K) + trueTestPermitsTrueOther = false; + break; + } + break; + } + } + + private void CheckConsistentTypeTestsDecision( + IValueSet? whenFalseValues, + SyntaxNode syntax, + ref bool trueTestPermitsTrueOther, + ref bool falseTestPermitsTrueOther, + ref bool trueTestImpliesTrueOther, + ref bool falseTestImpliesTrueOther, + TypeSymbol t1Type, + TypeSymbol t2Type) + { + var useSiteInfo = new CompoundUseSiteInfo(_diagnostics, _compilation.Assembly); + ConstantValue? matches = ExpressionOfTypeMatchesPatternTypeForLearningFromSuccessfulTypeTest(_conversions, t1Type, t2Type, ref useSiteInfo); + if (matches == ConstantValue.False) + { + // If T1 could never be T2 + // v is T1 --> !(v is T2) + trueTestPermitsTrueOther = false; + } + else if (matches == ConstantValue.True) + { + // If T1: T2 + // v is T1 --> v is T2 + trueTestImpliesTrueOther = true; + } + + // If every T2 is a T1, then failure of T1 implies failure of T2. + matches = Binder.ExpressionOfTypeMatchesPatternType(_conversions, t2Type, t1Type, ref useSiteInfo, out _); + + if (matches == ConstantValue.True) + { + // If T2: T1 + // !(v is T1) --> !(v is T2) + falseTestPermitsTrueOther = false; + } + else if (!_forLowering && whenFalseValues is TypeUnionValueSet whenFalseUnionSet && + whenFalseUnionSet.TypeMatchesAllValuesIfAny(t2Type, ref useSiteInfo)) + { + // We don't know for sure that test for T2 is going to match, but if it fails, + // that means that some previous test must match and we simply cannot get to this test. + // In other words, this test is either unreachable or it will succeed. + // Either way, for the purposes of exhaustiveness check, default branch won't be taken on this path, + // and we record this fact below by setting 'falseTestImpliesTrueOther' to true. + falseTestImpliesTrueOther = true; + _suitableForLowering = false; + } + + _diagnostics.Add(syntax, useSiteInfo); + } + + private enum UnionTestKind + { + None, + NonNullTest, + TypeTest, + NullTest, + } + + private bool CheckConsistentUnionDecision( + BoundDagTest test, + BoundDagTest other, + SyntaxNode syntax, + out bool trueTestPermitsTrueOther, + out bool falseTestPermitsTrueOther, + out bool trueTestImpliesTrueOther, + out bool falseTestImpliesTrueOther) + { + // innocent until proven guilty + trueTestPermitsTrueOther = true; + falseTestPermitsTrueOther = true; + trueTestImpliesTrueOther = false; + falseTestImpliesTrueOther = false; + + if (!_forLowering) + { + return false; + } + + BoundDagTemp? testUnionInstance; + TypeSymbol? testTargetType; + UnionTestKind testKind = getUnionTestKind(test, out testTargetType, out testUnionInstance); + + if (testKind == UnionTestKind.None) + { + return false; + } + + Debug.Assert(testUnionInstance != null); + Debug.Assert(testKind != UnionTestKind.TypeTest || testTargetType is not null); + + BoundDagTemp? otherUnionInstance; + TypeSymbol? otherTargetType; + UnionTestKind otherKind = getUnionTestKind(other, out otherTargetType, out otherUnionInstance); + + if (otherKind == UnionTestKind.None) + { + return false; + } + + Debug.Assert(otherUnionInstance != null); + Debug.Assert(otherKind != UnionTestKind.TypeTest || otherTargetType is not null); + + if (!testUnionInstance.Type.Equals(otherUnionInstance.Type, TypeCompareKind.AllIgnoreOptions)) + { + return false; + } + + // The logic used here is duplicated from CheckConsistentDecision. + switch (testKind) + { + case UnionTestKind.NonNullTest: + // https://github.com/dotnet/roslyn/issues/82636: Tests do not observe significance of this path + switch (otherKind) + { + case UnionTestKind.NullTest: + // v != null --> !(v == null) + trueTestPermitsTrueOther = false; + // !(v != null) --> v == null + falseTestImpliesTrueOther = true; + break; + case UnionTestKind.NonNullTest: + // v != null --> v != null + trueTestImpliesTrueOther = true; + // !(v != null) --> !(v != null) + falseTestPermitsTrueOther = false; + break; + case UnionTestKind.TypeTest: + // !(v != null) --> !(v is T) + falseTestPermitsTrueOther = false; + break; + default: + throw ExceptionUtilities.UnexpectedValue(otherKind); + } + break; + case UnionTestKind.TypeTest: + switch (otherKind) + { + case UnionTestKind.NonNullTest: + // v is T --> v != null + trueTestImpliesTrueOther = true; + break; + case UnionTestKind.TypeTest: + Debug.Assert(testTargetType is not null); + Debug.Assert(otherTargetType is not null); + CheckConsistentTypeTestsDecision(whenFalseValues: null, syntax, ref trueTestPermitsTrueOther, ref falseTestPermitsTrueOther, ref trueTestImpliesTrueOther, ref falseTestImpliesTrueOther, testTargetType, otherTargetType); + break; + case UnionTestKind.NullTest: + { + // v is T --> !(v == null) + trueTestPermitsTrueOther = false; + } + break; + default: + throw ExceptionUtilities.UnexpectedValue(otherKind); + } + break; + case UnionTestKind.NullTest: + switch (otherKind) + { + case UnionTestKind.NonNullTest: + // https://github.com/dotnet/roslyn/issues/82636: Tests do not observe significance of this path + // v == null --> !(v != null) + trueTestPermitsTrueOther = false; + // !(v == null) --> v != null + falseTestImpliesTrueOther = true; + break; + case UnionTestKind.TypeTest: + // v == null --> !(v is T) + trueTestPermitsTrueOther = false; + break; + case UnionTestKind.NullTest: + // https://github.com/dotnet/roslyn/issues/82636: Tests do not observe significance of this path + // v == null --> v == null + trueTestImpliesTrueOther = true; + // !(v == null) --> !(v == null) + falseTestPermitsTrueOther = false; + break; + default: + throw ExceptionUtilities.UnexpectedValue(otherKind); + } + break; + default: + throw ExceptionUtilities.UnexpectedValue(testKind); + } + + return IsSameEntity(testUnionInstance, otherUnionInstance); + + static UnionTestKind getUnionTestKind(BoundDagTest test, out TypeSymbol? targetType, out BoundDagTemp? testUnionInstance) + { + if (isAnyUnionValueNullTest(test, out testUnionInstance)) + { + targetType = null; + return UnionTestKind.NullTest; + } + else if (isAnyUnionValueNonNullTest(test, out testUnionInstance)) + { + targetType = null; + return UnionTestKind.NonNullTest; + } + else if (isAnyUnionValueTypeTest(test, out targetType, out testUnionInstance)) + { + return UnionTestKind.TypeTest; + } + else if (isUnionHasValueTest(test, out bool hasValueSense, out testUnionInstance)) + { + targetType = null; + + if (hasValueSense) + { + return UnionTestKind.NonNullTest; // https://github.com/dotnet/roslyn/issues/82636: Cover this code path + } + else + { + return UnionTestKind.NullTest; + } + } + else if (IsUnionTryGetValueTest(test, out targetType, out testUnionInstance, out _)) + { + return UnionTestKind.TypeTest; + } + + targetType = null; + return UnionTestKind.None; + } + + static bool isAnyUnionValueNullTest(BoundDagTest test, [NotNullWhen(true)] out BoundDagTemp? testUnionInstance) + { + testUnionInstance = null; + return test is BoundDagExplicitNullTest && IsAnyUnionValue(NotTypeEvaluationInput(test.Input), out testUnionInstance); + } + + static bool isAnyUnionValueNonNullTest(BoundDagTest test, [NotNullWhen(true)] out BoundDagTemp? testUnionInstance) + { + testUnionInstance = null; + return test is BoundDagNonNullTest && IsAnyUnionValue(NotTypeEvaluationInput(test.Input), out testUnionInstance); + } + + static bool isAnyUnionValueTypeTest(BoundDagTest test, [NotNullWhen(true)] out TypeSymbol? targetType, [NotNullWhen(true)] out BoundDagTemp? testUnionInstance) + { + if (test is BoundDagTypeTest typeTest && IsAnyUnionValue(NotTypeEvaluationInput(test.Input), out testUnionInstance)) + { + targetType = typeTest.Type; + return true; + } + + targetType = null; + testUnionInstance = null; + return false; + } + + static bool isUnionHasValueTest(BoundDagTest test, out bool sense, [NotNullWhen(true)] out BoundDagTemp? testUnionInstance) + { + if (test is BoundDagValueTest hasValueTest && IsUnionHasValue(test.Input, out testUnionInstance)) + { + Debug.Assert((hasValueTest.Value == ConstantValue.True || hasValueTest.Value == ConstantValue.False)); + if (hasValueTest.Value == ConstantValue.True || hasValueTest.Value == ConstantValue.False) { - case BoundDagNonNullTest n2: - if (n2.IsExplicitTest) - foundExplicitNullTest = true; - // v == null --> !(v != null) - trueTestPermitsTrueOther = false; - // !(v == null) --> v != null - falseTestImpliesTrueOther = true; - break; - case BoundDagTypeTest _: - // v == null --> !(v is T) - trueTestPermitsTrueOther = false; - break; - case BoundDagExplicitNullTest _: - foundExplicitNullTest = true; - // v == null --> v == null - trueTestImpliesTrueOther = true; - // !(v == null) --> !(v == null) - falseTestPermitsTrueOther = false; - break; - case BoundDagValueTest _: - // v == null --> !(v == K) - trueTestPermitsTrueOther = false; - break; + sense = hasValueTest.Value == ConstantValue.True; + return true; } - break; + } + + sense = false; + testUnionInstance = null; + return false; + } + } + + private static bool IsUnionTryGetValueTest(BoundDagTest test, [NotNullWhen(true)] out TypeSymbol? targetType, [NotNullWhen(true)] out BoundDagTemp? testUnionInstance, [NotNullWhen(true)] out BoundDagDeconstructEvaluation? tryGetValueEvaluation) + { + if (test is BoundDagValueTest tryGetValueTest && IsUnionTryGetValueReturn(test.Input, out targetType, out testUnionInstance, out tryGetValueEvaluation)) + { + bool isTrue = tryGetValueTest.Value == ConstantValue.True; + Debug.Assert(isTrue); // The only form of the test that we construct + return isTrue; } + + targetType = null; + testUnionInstance = null; + tryGetValueEvaluation = null; + return false; } /// Returns true if the tests are related i.e. they have the same input, otherwise false. - /// The pre-condition under which these tests are related. - /// A possible assignment node which will correspond two non-identical but related test inputs. private bool CheckInputRelation( - SyntaxNode syntax, - DagState state, BoundDagTest test, - BoundDagTest other, - out Tests relationCondition, - out Tests relationEffect) + BoundDagTest other) { - relationCondition = Tests.True.Instance; - relationEffect = Tests.True.Instance; - // If inputs are identical, we don't need to do any further check. if (test.Input == other.Input) { @@ -1481,40 +2392,97 @@ other is not (BoundDagNonNullTest or BoundDagExplicitNullTest) && return false; } - BoundDagTemp s1Input = OriginalInput(test.Input); - BoundDagTemp s2Input = OriginalInput(other.Input); - // Loop through the input chain for both tests at the same time and check if there's - // any pair of indexers in the path that could relate depending on the length value. - ArrayBuilder? conditions = null; + return IsSameEntity(test.Input, other.Input); + } + + private static bool IsSameEntity(BoundDagTemp input1, BoundDagTemp input2) + { + BoundDagTemp s1Input = OriginalInput(input1); + BoundDagTemp s2Input = OriginalInput(input2); while (s1Input.Index == s2Input.Index) { switch (s1Input.Source, s2Input.Source) { - // We should've skipped all type evaluations at this point. - case (BoundDagTypeEvaluation, _): - case (_, BoundDagTypeEvaluation): + case (BoundDagPassThroughEvaluation s1, _): + s1Input = s1.Input; + continue; + + case (_, BoundDagPassThroughEvaluation s2): + s2Input = s2.Input; + continue; + + case (BoundDagTypeEvaluation s1, BoundDagTypeEvaluation s2): + + // We are going to treat any Union value with a type evaluation on top as related + // regardless of the specific API used to get the value as long as the union + // instances are related. + if (IsAnyUnionValue(s1.Input, out BoundDagTemp? s1UnionInstance) && + IsAnyUnionValue(s2.Input, out BoundDagTemp? s2UnionInstance)) + { + if (s1UnionInstance.Type.Equals(s2UnionInstance.Type, TypeCompareKind.AllIgnoreOptions)) + { + s1Input = OriginalInput(s1UnionInstance); + s2Input = OriginalInput(s2UnionInstance); + continue; + } + + // unrelated + return false; + } + + // We should've skipped all type evaluations at this point. throw ExceptionUtilities.Unreachable(); - // If we have found two identical evaluations as the source (possibly null), inputs can be considered related. - case var (s1, s2) when s1 == s2: - if (conditions != null) + case (BoundDagTypeEvaluation s1, _): + + if (IsUnionValue(s1.Input, out _)) + { + s1Input = s1.Input; + continue; + } + + if (IsUnionTryGetValueValue(s1.Input, out _)) + { + // unrelated + return false; + } + + // We should've skipped all type evaluations at this point. + throw ExceptionUtilities.Unreachable(); + + case (_, BoundDagTypeEvaluation s2): + + if (IsUnionValue(s2.Input, out _)) + { + s2Input = s2.Input; + continue; + } + + if (IsUnionTryGetValueValue(s2.Input, out _)) { - relationCondition = Tests.AndSequence.Create(conditions); - // At this point, we have determined that two non-identical inputs refer to the same element. - // We represent this correspondence with an assignment node in order to merge the remaining values. - // If tests are related unconditionally, we won't need to do so as the remaining values are updated right away. - relationEffect = new Tests.One(new BoundDagAssignmentEvaluation(syntax, target: other.Input, input: test.Input)); + // unrelated + return false; } + + // We should've skipped all type evaluations at this point. + throw ExceptionUtilities.Unreachable(); + + // If we have found two identical evaluations as the source (possibly null), inputs can be considered related. + case var (s1, s2) when s1 == s2: return true; // Even though the two tests appear unrelated (with different inputs), // it is possible that they are in fact related under certain conditions. // For instance, the inputs [0] and [^1] point to the same element when length is 1. - case (BoundDagIndexerEvaluation s1, BoundDagIndexerEvaluation s2): + case (BoundDagIndexerEvaluation s1, BoundDagIndexerEvaluation s2) when s1.IndexerType.Equals(s2.IndexerType, TypeCompareKind.AllIgnoreOptions): // Take the top-level input and normalize indices to account for indexer accesses inside a slice. // For instance [0] in nested list pattern [ 0, ..[$$], 2 ] refers to [1] in the containing list. (s1Input, BoundDagTemp s1LengthTemp, int s1Index) = GetCanonicalInput(s1); (s2Input, BoundDagTemp s2LengthTemp, int s2Index) = GetCanonicalInput(s2); + + s1Input = OriginalInput(s1Input); + s2Input = OriginalInput(s2Input); + // Ignore input source as it will be matched in the subsequent iterations. if (s1Input.Index == s2Input.Index) { @@ -1523,36 +2491,9 @@ other is not (BoundDagNonNullTest or BoundDagExplicitNullTest) && { continue; } - - if (s1Index < 0 != s2Index < 0) - { - Debug.Assert(state.RemainingValues.ContainsKey(s1LengthTemp)); - var lengthValues = (IValueSet)state.RemainingValues[s1LengthTemp]; - // We do not expect an empty set here because an indexer evaluation is always preceded by - // a length test of which an impossible match would have made the rest of the tests unreachable. - Debug.Assert(!lengthValues.IsEmpty); - - // Compute the length value that would make these two indices point to the same element. - int lengthValue = s1Index < 0 ? s2Index - s1Index : s1Index - s2Index; - if (lengthValues.All(BinaryOperatorKind.Equal, lengthValue)) - { - // If the length is known to be exact, the two are considered to point to the same element. - continue; - } - - if (!_forLowering && lengthValues.Any(BinaryOperatorKind.Equal, lengthValue)) - { - // Otherwise, we add a test to make the result conditional on the length value. - (conditions ??= ArrayBuilder.GetInstance()).Add(new Tests.One(new BoundDagValueTest(syntax, ConstantValue.Create(lengthValue), s1LengthTemp))); - continue; - } - } } break; - // If the sources are equivalent (ignoring their input), it's still possible to find a pair of indexers that could relate. - // For example, the subpatterns in `[.., { E: subpat }] or [{ E: subpat }]` are being applied to the same element in the list. - // To account for this scenario, we walk up all the inputs as long as we see equivalent evaluation nodes in the path. case (BoundDagEvaluation s1, BoundDagEvaluation s2) when s1.IsEquivalentTo(s2): s1Input = OriginalInput(s1.Input); s2Input = OriginalInput(s2.Input); @@ -1561,11 +2502,156 @@ other is not (BoundDagNonNullTest or BoundDagExplicitNullTest) && break; } - // tests are unrelated - conditions?.Free(); + // unrelated return false; } + /// + /// Determine if two evaluations, if successfully performed, producing the same value. + /// This function is used to inplement equality of evaluations, which affects how states are merged + /// during Dag construction. + /// The logic is stricter than , in that it doesn't allow for result types and + /// input types to differ. It is however skips intermediate type evaluations that might be performed during + /// the evaluation process for an input for other kinds of evaluations. + /// The strictness around the types is necessary because the result must not depend on the fact + /// whether a specific evaluation is reachable according to the tests that are performed + /// before the evaluation. The helper is more lenient because + /// it is allowed to rely on the fact that an evaluation wouldn't be reachable when preceeding tests fail. + /// + internal static bool IsEqualEvaluation(BoundDagEvaluation? s1Source, BoundDagEvaluation? s2Source) + { + while (true) + { + switch (s1Source, s2Source) + { + // If we have two identical evaluations + case var (s1, s2) when s1 == s2: + return true; + + // Even though the two tests appear unrelated (with different inputs), + // it is possible that they are in fact related under certain conditions. + // For instance, the inputs [0] and [^1] point to the same element when length is 1. + case (BoundDagIndexerEvaluation s1, BoundDagIndexerEvaluation s2) when s1.IndexerType.Equals(s2.IndexerType, TypeCompareKind.AllIgnoreOptions): + { + // Take the top-level input and normalize indices to account for indexer accesses inside a slice. + // For instance [0] in nested list pattern [ 0, ..[$$], 2 ] refers to [1] in the containing list. + (BoundDagTemp s1Input, BoundDagTemp s1LengthTemp, int s1Index) = GetCanonicalInput(s1); + (BoundDagTemp s2Input, BoundDagTemp s2LengthTemp, int s2Index) = GetCanonicalInput(s2); + + if (s1Index == s2Index) + { + return s1Input.Equals(s2Input); + } + + // different + return false; + } + + case (BoundDagPassThroughEvaluation s1, BoundDagPassThroughEvaluation s2): + { + // PassThrough nodes are used to prevent unification of states that access the same Union value + // but through different Union APIs. Therefore, they are considered equivalent only + // when that pass through a value of the same type and the value originates from an equivalent source + // ignoring any type evaluations in between. + Debug.Assert(s1.Input.Source is BoundDagTypeEvaluation); + Debug.Assert(s2.Input.Source is BoundDagTypeEvaluation); + + BoundDagTemp s1Input = NotTypeEvaluationInput(s1.Input.Source); + BoundDagTemp s2Input = NotTypeEvaluationInput(s2.Input.Source); + return s1.Input.Type.Equals(s2.Input.Type, TypeCompareKind.AllIgnoreOptions) && s1Input.Equals(s2Input); + } + + case (BoundDagTypeEvaluation s1, BoundDagTypeEvaluation s2): + { + if (!s1.Type.Equals(s2.Type, TypeCompareKind.AllIgnoreOptions)) + { + // different + return false; + } + + BoundDagTemp s1OriginalInput = NotTypeEvaluationInput(s1); + BoundDagTemp s2OriginalInput = NotTypeEvaluationInput(s2); + + // Ignore Union access API differences as long as they access the same Union instance. + // For instance, TryGetValue and HasValue can be considered equivalent for this purpose. + // The same goes for different TryGetValue overloads. This allows us to better unify states + // that come after the values are extracted from the instances. + // We intentionally add an identity type evaluation on top of TryGetValue output parameters + // so that we get through this code for them and get advantage of the unification. + if (IsAnyUnionValue(s1OriginalInput, out BoundDagTemp? s1UnionInstance) && + IsAnyUnionValue(s2OriginalInput, out BoundDagTemp? s2UnionInstance)) + { + s1OriginalInput = s1UnionInstance; + s2OriginalInput = s2UnionInstance; + } + + return s1OriginalInput.Equals(s2OriginalInput); + } + + case (BoundDagTypeEvaluation s1, BoundDagIndexerEvaluation or BoundDagFieldEvaluation or BoundDagPropertyEvaluation or BoundDagIndexEvaluation or BoundDagSliceEvaluation): // s2Source can be anything with an output, obtainable via MakeResultTemp(). + { + if (!s1.Type.Equals(s2Source.MakeResultTemp().Type, TypeCompareKind.AllIgnoreOptions)) + { + // different + return false; + } + + s1Source = SkipAllTypeEvaluations(s1); + continue; + } + + case (BoundDagIndexerEvaluation or BoundDagFieldEvaluation or BoundDagPropertyEvaluation or BoundDagIndexEvaluation or BoundDagSliceEvaluation, BoundDagTypeEvaluation s2): + { + if (!s2.Type.Equals(s1Source.MakeResultTemp().Type, TypeCompareKind.AllIgnoreOptions)) + { + // different + return false; + } + + s2Source = SkipAllTypeEvaluations(s2); + continue; + } + + case (BoundDagEvaluation s1, BoundDagEvaluation s2) when s1.IsEquivalentTo(s2): + { + return s1.Input.Equals(s2.Input); + } + + default: + // different + return false; + } + + throw ExceptionUtilities.Unreachable(); + } + } + + private static bool IsAnyUnionValue(BoundDagTemp input, [NotNullWhen(true)] out BoundDagTemp? unionInstance) + { + return IsUnionTryGetValueValue(input, out unionInstance) || IsUnionValue(input, out unionInstance); + } + + internal static BoundDagEvaluation? SkipAllTypeEvaluations(BoundDagTypeEvaluation typeEval) + { + return NotTypeEvaluationInput(typeEval).Source; + } + + internal static BoundDagTemp NotTypeEvaluationInput(BoundDagTemp input) + { + while (input.Source is BoundDagTypeEvaluation source) + { + Debug.Assert(input.Index == 0); + input = source.Input; + } + + return input; + } + + internal static BoundDagTemp NotTypeEvaluationInput(BoundDagTest test) + { + return NotTypeEvaluationInput(test.Input); + } + /// /// Determine what we can learn from one successful runtime type test about another planned /// runtime type test for the purpose of building the decision tree. @@ -1580,12 +2666,13 @@ other is not (BoundDagNonNullTest or BoundDagExplicitNullTest) && /// of a switch (on the one hand) and a series of if-then-else statements (on the other). /// See, for example, https://github.com/dotnet/roslyn/issues/35661 /// - private ConstantValue? ExpressionOfTypeMatchesPatternTypeForLearningFromSuccessfulTypeTest( + internal static ConstantValue? ExpressionOfTypeMatchesPatternTypeForLearningFromSuccessfulTypeTest( + ConversionsBase conversions, TypeSymbol expressionType, TypeSymbol patternType, ref CompoundUseSiteInfo useSiteInfo) { - ConstantValue result = Binder.ExpressionOfTypeMatchesPatternType(_conversions, expressionType, patternType, ref useSiteInfo, out Conversion conversion); + ConstantValue result = Binder.ExpressionOfTypeMatchesPatternType(conversions, expressionType, patternType, ref useSiteInfo, out Conversion conversion); return (!conversion.Exists && isRuntimeSimilar(expressionType, patternType)) ? null // runtime and compile-time test behavior differ. Pretend we don't know what happens. : result; @@ -1713,7 +2800,21 @@ int tempIdentifier(BoundDagEvaluation? e) string tempName(BoundDagTemp t) { - return $"t{tempIdentifier(t.Source)}"; + string name = $"t{tempIdentifier(t.Source)}"; + + if (t.Source is BoundDagDeconstructEvaluation) + { + if (t.Index == -1) + { + return name + ".ReturnItem"; + } + else + { + return name + ".Item" + (t.Index + 1).ToString(); + } + } + + return name; } var resultBuilder = PooledStringBuilder.GetInstance(); @@ -1963,9 +3064,9 @@ public void ClearAndFree() /// heuristic we can change to adjust the quality of the generated decision automaton. /// See https://www.cs.tufts.edu/~nr/cs257/archive/norman-ramsey/match.pdf for some ideas. /// - internal BoundDagTest ComputeSelectedTest() + internal BoundDagTest ComputeSelectedTest(bool forLowering, ref bool suitableForLowering) { - return Cases[0].RemainingTests.ComputeSelectedTest(); + return Cases[0].RemainingTests.ComputeSelectedTest(forLowering, ref suitableForLowering); } internal void UpdateRemainingValues(ImmutableDictionary newRemainingValues) @@ -2117,8 +3218,127 @@ public abstract void Filter( out Tests whenTrue, out Tests whenFalse, ref bool foundExplicitNullTest); - public virtual BoundDagTest ComputeSelectedTest() => throw ExceptionUtilities.Unreachable(); - public virtual Tests RemoveEvaluation(BoundDagEvaluation e) => this; + public virtual BoundDagTest ComputeSelectedTest(bool forLowering, ref bool suitableForLowering) => throw ExceptionUtilities.Unreachable(); + + protected readonly struct RemoveEvaluationAndUpdateTempReferencesResult + { + /// + /// Temps are updated and evaluation is removed + /// + public readonly Tests FinalResult; + public readonly ImmutableDictionary FinalTempMap; + + /// + /// Evaluation isn't removed, but temps are updated + /// + public readonly Tests? ConditionToUseFinalResult; + public readonly Tests? TempsUpdatedResult; + + public RemoveEvaluationAndUpdateTempReferencesResult( + Tests finalResult, + ImmutableDictionary finalTempMap, + Tests? conditionToUseFinalResult, + Tests? tempsUpdatedResult) + { + Debug.Assert((conditionToUseFinalResult is null) == (tempsUpdatedResult is null)); + Debug.Assert((conditionToUseFinalResult is null) || (tempsUpdatedResult is One(BoundDagIndexerEvaluation))); + this.FinalResult = finalResult; + this.FinalTempMap = finalTempMap; + this.ConditionToUseFinalResult = conditionToUseFinalResult; + this.TempsUpdatedResult = tempsUpdatedResult; + } + } + + protected abstract RemoveEvaluationAndUpdateTempReferencesResult RemoveEvaluationAndUpdateTempReferencesCore(DecisionDagBuilder dagBuilder, DagState state, ImmutableArray bindings, ImmutableDictionary tempMap, BoundDagEvaluation e); + + public Tests RemoveEvaluation(DecisionDagBuilder dagBuilder, DagState state, ImmutableArray bindings, BoundDagEvaluation e) + { + return RemoveEvaluationAndUpdateTempReferences(dagBuilder, state, bindings, ImmutableDictionary.Empty, e); + } + + protected Tests RemoveEvaluationAndUpdateTempReferences(DecisionDagBuilder dagBuilder, DagState state, ImmutableArray bindings, ImmutableDictionary tempMap, BoundDagEvaluation e) + { + RemoveEvaluationAndUpdateTempReferencesResult rewriteResult = RemoveEvaluationAndUpdateTempReferencesCore(dagBuilder, state, bindings, tempMap, e); + + if (rewriteResult.FinalTempMap == tempMap && rewriteResult.ConditionToUseFinalResult is null) + { + return rewriteResult.FinalResult; + } + + var finalResult = ArrayBuilder.GetInstance(2); + + finalResult.Add(rewriteResult.FinalResult); + AddBindingsPatchingAssignments(bindings, tempMap, rewriteResult.FinalTempMap, finalResult); + + if (rewriteResult.ConditionToUseFinalResult is null) + { + // Microsoft.CodeAnalysis.CSharp.UnitTests.PatternMatchingTests_ListPatterns.Exhaustiveness_01 hits this case + // Trivial case. + return AndSequence.Create(finalResult); + } + + // Microsoft.CodeAnalysis.CSharp.UnitTests.PatternMatchingTests_ListPatterns.ListPattern_Negated_05 hits this case + Debug.Assert(rewriteResult.TempsUpdatedResult is not null); + + // + // We need to replace the node with a test of the following shape: + // + // OrSequence( + // AndSequence( + // ConditionToUseFinalResult, + // finalResult + // ), + // AndSequence( + // Not (ConditionToUseFinalResult), + // TempsUpdatedResult + // ) + // ) + // + + finalResult.Insert(0, rewriteResult.ConditionToUseFinalResult); + + return OrSequence.Create( + AndSequence.Create(finalResult), + AndSequence.Create(Not.Create(rewriteResult.ConditionToUseFinalResult), rewriteResult.TempsUpdatedResult)); + } + + private static void AddBindingsPatchingAssignments(ImmutableArray bindings, ImmutableDictionary oldTempMap, ImmutableDictionary newTempMap, ArrayBuilder assignments) + { + if (oldTempMap == newTempMap) + { + return; + } + + foreach (BoundPatternBinding b in bindings) + { + if (TryGetTempReplacement(newTempMap, b.TempContainingValue, out BoundDagTemp? useValueFrom)) + { + if (!TryGetTempReplacement(oldTempMap, b.TempContainingValue, out BoundDagTemp? oldReplacement) || + !oldReplacement.Equals(useValueFrom)) + { + Debug.Assert(!b.TempContainingValue.Equals(useValueFrom)); + assignments.Add(new Tests.One(new BoundDagAssignmentEvaluation(useValueFrom.Syntax, b.TempContainingValue, useValueFrom))); + } + } + } + } + + private static bool TryGetTempReplacement(ImmutableDictionary tempMap, BoundDagTemp oldTemp, [NotNullWhen(true)] out BoundDagTemp? newTemp) + { + if (tempMap.TryGetValue(oldTemp, out newTemp)) + { + // Get to the final temp in the chain + while (tempMap.TryGetValue(newTemp, out BoundDagTemp? nextTemp)) + { + newTemp = nextTemp; + } + + return true; + } + + return false; + } + /// /// Rewrite nested length tests in slice subpatterns to check the top-level length property instead. /// @@ -2144,6 +3364,9 @@ public override void Filter( { whenTrue = whenFalse = this; } + + protected override RemoveEvaluationAndUpdateTempReferencesResult RemoveEvaluationAndUpdateTempReferencesCore(DecisionDagBuilder builder, DagState state, ImmutableArray bindings, ImmutableDictionary tempMap, BoundDagEvaluation e) + => new RemoveEvaluationAndUpdateTempReferencesResult(this, tempMap, conditionToUseFinalResult: null, tempsUpdatedResult: null); } /// @@ -2165,6 +3388,9 @@ public override void Filter( { whenTrue = whenFalse = this; } + + protected override RemoveEvaluationAndUpdateTempReferencesResult RemoveEvaluationAndUpdateTempReferencesCore(DecisionDagBuilder builder, DagState state, ImmutableArray bindings, ImmutableDictionary tempMap, BoundDagEvaluation e) + => new RemoveEvaluationAndUpdateTempReferencesResult(this, tempMap, conditionToUseFinalResult: null, tempsUpdatedResult: null); } /// @@ -2189,68 +3415,62 @@ public override void Filter( { SyntaxNode syntax = test.Syntax; BoundDagTest other = this.Test; - if (other is BoundDagEvaluation || - !builder.CheckInputRelation(syntax, state, test, other, - relationCondition: out Tests relationCondition, - relationEffect: out Tests relationEffect)) + if (other is BoundDagEvaluation) { - // if this is an evaluation or the tests are for unrelated things, - // there cannot be any implications from one to the other. + // if this is an evaluation, there cannot be any implications from one to the other. whenTrue = whenFalse = this; return; } - builder.CheckConsistentDecision( - test: test, - other: other, - whenTrueValues: whenTrueValues, - whenFalseValues: whenFalseValues, - syntax: syntax, - trueTestPermitsTrueOther: out bool trueDecisionPermitsTrueOther, - falseTestPermitsTrueOther: out bool falseDecisionPermitsTrueOther, - trueTestImpliesTrueOther: out bool trueDecisionImpliesTrueOther, - falseTestImpliesTrueOther: out bool falseDecisionImpliesTrueOther, - foundExplicitNullTest: ref foundExplicitNullTest); - - Debug.Assert(relationEffect is True or One(BoundDagAssignmentEvaluation)); - - // Given: - // - // - "test" as a test that has already occurred, - // - "other" as a subsequent test, - // - S as a possible side-effect (expected to always evaluate to True), - // - and P as a pre-condition under which we need to evaluate S, - // - // we proceed as follows: - // - // - If "test" being true proves "other" to be also true, we rewrite "other" as ((P && S) || other), - // because we have determined that on this branch, "other" would always succeed if the pre-condition is met. - // Note: If there is no pre-condition, i.e. P is True, the above will be reduced to True which means "other" is insignificant. - // - // - If "test" being true proves "other" to be false, we rewrite "other" as (!(P && S) && other), - // because we have determined that on this branch, "other" would never succeed if the pre-condition is met. - // Note: If there is no pre-condition, i.e. P is True, the above will be reduced to False which means "other" is impossible. - // - // - Otherwise, we rewrite "other" as ((!P || S) && other) to preserve the side-effect if the pre-condition is met, - // because we have determined that there were no logical implications from one to the other on this branch. - // Note: If there is no pre-condition, i.e. P is True, "other" is not rewritten which means the two are considered independent. - // - whenTrue = rewrite(trueDecisionImpliesTrueOther, trueDecisionPermitsTrueOther, relationCondition, relationEffect, this); + bool trueDecisionPermitsTrueOther; + bool falseDecisionPermitsTrueOther; + bool trueDecisionImpliesTrueOther; + bool falseDecisionImpliesTrueOther; + + if (builder.CheckInputRelation(test, other)) + { + builder.CheckConsistentDecision( + test: test, + other: other, + whenTrueValues: whenTrueValues, + whenFalseValues: whenFalseValues, + syntax: syntax, + trueTestPermitsTrueOther: out trueDecisionPermitsTrueOther, + falseTestPermitsTrueOther: out falseDecisionPermitsTrueOther, + trueTestImpliesTrueOther: out trueDecisionImpliesTrueOther, + falseTestImpliesTrueOther: out falseDecisionImpliesTrueOther, + foundExplicitNullTest: ref foundExplicitNullTest); + + split(trueDecisionPermitsTrueOther, falseDecisionPermitsTrueOther, trueDecisionImpliesTrueOther, falseDecisionImpliesTrueOther, out whenTrue, out whenFalse); + return; + } + + if (builder.CheckConsistentUnionDecision( + test: test, + other: other, + syntax: syntax, + trueTestPermitsTrueOther: out trueDecisionPermitsTrueOther, + falseTestPermitsTrueOther: out falseDecisionPermitsTrueOther, + trueTestImpliesTrueOther: out trueDecisionImpliesTrueOther, + falseTestImpliesTrueOther: out falseDecisionImpliesTrueOther)) + { + split(trueDecisionPermitsTrueOther, falseDecisionPermitsTrueOther, trueDecisionImpliesTrueOther, falseDecisionImpliesTrueOther, out whenTrue, out whenFalse); + return; + } - // Similarly for the opposite branch when "test" is false. - whenFalse = rewrite(falseDecisionImpliesTrueOther, falseDecisionPermitsTrueOther, relationCondition, relationEffect, this); + // if this is the tests are for unrelated things, + // there cannot be any implications from one to the other. + whenTrue = whenFalse = this; + return; - static Tests rewrite(bool decisionImpliesTrueOther, bool decisionPermitsTrueOther, Tests relationCondition, Tests relationEffect, Tests other) + void split(bool trueDecisionPermitsTrueOther, bool falseDecisionPermitsTrueOther, bool trueDecisionImpliesTrueOther, bool falseDecisionImpliesTrueOther, out Tests whenTrue, out Tests whenFalse) { - return decisionImpliesTrueOther - ? OrSequence.Create(AndSequence.Create(relationCondition, relationEffect), other) - : !decisionPermitsTrueOther - ? AndSequence.Create(Not.Create(AndSequence.Create(relationCondition, relationEffect)), other) - : AndSequence.Create(OrSequence.Create(Not.Create(relationCondition), relationEffect), other); + whenTrue = trueDecisionImpliesTrueOther ? Tests.True.Instance : trueDecisionPermitsTrueOther ? this : (Tests)Tests.False.Instance; + whenFalse = falseDecisionImpliesTrueOther ? Tests.True.Instance : falseDecisionPermitsTrueOther ? this : (Tests)Tests.False.Instance; } } - public override BoundDagTest ComputeSelectedTest() => this.Test; - public override Tests RemoveEvaluation(BoundDagEvaluation e) => e.Equals(Test) ? Tests.True.Instance : (Tests)this; + + public override BoundDagTest ComputeSelectedTest(bool forLowering, ref bool suitableForLowering) => this.Test; public override string Dump(Func dump) => dump(this.Test); public override bool Equals(object? obj) => this == obj || obj is One other && this.Test.Equals(other.Test); public override int GetHashCode() => this.Test.GetHashCode(); @@ -2296,11 +3516,11 @@ public override Tests RewriteNestedLengthTests() static Tests? knownResult(BinaryOperatorKind relation, ConstantValue constant, int offset) { var fac = ValueSetFactory.ForLength; - var possibleValues = fac.Related(BinaryOperatorKind.LessThanOrEqual, int.MaxValue - offset); - var lengthValues = fac.Related(relation, constant); - if (lengthValues.Intersect(possibleValues).IsEmpty) + IConstantValueSet possibleValues = fac.Related(BinaryOperatorKind.LessThanOrEqual, int.MaxValue - offset); + IConstantValueSet lengthValues = fac.Related(relation, constant); + if (((IConstantValueSet)lengthValues.Intersect(possibleValues)).IsEmpty) return False.Instance; - if (lengthValues.Complement().Intersect(possibleValues).IsEmpty) + if (((IConstantValueSet)lengthValues.Complement().Intersect(possibleValues)).IsEmpty) return True.Instance; return null; } @@ -2315,6 +3535,441 @@ static ConstantValue safeAdd(ConstantValue constant, int offset) return ConstantValue.Create(offset > (int.MaxValue - value) ? int.MaxValue : value + offset); } } + + protected override RemoveEvaluationAndUpdateTempReferencesResult RemoveEvaluationAndUpdateTempReferencesCore(DecisionDagBuilder builder, DagState state, ImmutableArray bindings, ImmutableDictionary tempMap, BoundDagEvaluation e) + { + if (e is BoundDagPassThroughEvaluation) + { + // PassThrough evaluations should not be removed, unless we are removing the same exact evaluation. + // Even if another PassThrough evaluation otherwise looks similar, it might still be necessary + // to stay to serve its purpose of delaying state unification. + Debug.Assert(tempMap.IsEmpty); + + if (Test == e) + { + return new RemoveEvaluationAndUpdateTempReferencesResult(Tests.True.Instance, tempMap, conditionToUseFinalResult: null, tempsUpdatedResult: null); + } + + return new RemoveEvaluationAndUpdateTempReferencesResult(this, tempMap, conditionToUseFinalResult: null, tempsUpdatedResult: null); + } + + var savedTempMap = tempMap; + var updatedTest = UpdateDagTempReferences(Test, ref tempMap); + if (e.Equals(updatedTest)) + { + return new RemoveEvaluationAndUpdateTempReferencesResult(Tests.True.Instance, tempMap, conditionToUseFinalResult: null, tempsUpdatedResult: null); + } + + var tempsUpdatedResultTempMap = tempMap; + var tempsUpdatedResult = this; + + if (!Test.Equals(updatedTest)) + { + tempsUpdatedResult = new One(updatedTest); + } + else + { + Debug.Assert(savedTempMap == tempsUpdatedResultTempMap); + } + + Tests finalResult = RemoveEvaluation(tempsUpdatedResult, builder, state, ref tempMap, e, out var condition); + + Debug.Assert(!finalResult.Equals(tempsUpdatedResult) || tempsUpdatedResultTempMap == tempMap); + Debug.Assert(condition is null || + (!finalResult.Equals(tempsUpdatedResult) && + Test is BoundDagIndexerEvaluation && + tempsUpdatedResult is One(BoundDagIndexerEvaluation))); + Debug.Assert(Test is not BoundDagIndexerEvaluation || + (tempsUpdatedResult == this && tempsUpdatedResultTempMap == savedTempMap) || + (finalResult == tempsUpdatedResult && condition is null)); + + if (condition is not null && savedTempMap != tempsUpdatedResultTempMap) + { + // The process of updating temps updated the underlying indexer evaluation. + // Then the process of removal made another update to that indexer evaluation. + // This means that the evaluation would somehow take an input that is calculated + // from itself, only then temps update would update the evaluation. + // But a circularity like this is not possible with indexer evaluations. + // In other words, it is not possible to evaluate an indexer and have an 'output' + // that is considered as the same entity as the 'input', so that the next indexer + // evaluation on the 'output' would be considered equivalent to the first one. + // This fact is important because only indexer evaluations can result in a conditional + // removal, and properly handling the temp map change resulting from the temps update + // in this case, while possible, will add an extra complexity to handle the case that + // we think is impossible. + Debug.Fail("Unexpected change in temp map during conditional removal of indexer evaluation."); + + // In case we somehow get here, it is safe to return the result of updating the temps only. + return new RemoveEvaluationAndUpdateTempReferencesResult(tempsUpdatedResult, tempsUpdatedResultTempMap, null, null); + } + + return new RemoveEvaluationAndUpdateTempReferencesResult( + finalResult, + tempMap, + condition, + condition is null ? null : tempsUpdatedResult); + } + + public static Tests RemoveEvaluation(One tests, DecisionDagBuilder builder, DagState state, ref ImmutableDictionary tempMap, BoundDagEvaluation e, out Tests? condition) + { + switch (e) + { + case BoundDagTypeEvaluation typeEval: + { + condition = null; + return RemoveTypeEvaluation(tests, builder, ref tempMap, typeEval); + } + case BoundDagIndexerEvaluation indexer: + { + return RemoveIndexerEvaluation(tests, builder, state, ref tempMap, indexer, out condition); + } + case BoundDagDeconstructEvaluation deconstruct: + { + condition = null; + return RemoveDeconstructEvaluation(tests, ref tempMap, deconstruct); + } + case BoundDagFieldEvaluation: + case BoundDagPropertyEvaluation: + case BoundDagIndexEvaluation: + case BoundDagSliceEvaluation: + { + condition = null; + return RemoveSimpleEvaluationWithResultTemp(tests, ref tempMap, e); + } + case BoundDagAssignmentEvaluation assignment: + { + condition = null; + return RemoveAssignmentEvaluation(tests, assignment); + } + default: + throw ExceptionUtilities.UnexpectedValue(e); + } + } + + private static bool IsEquivalentEvaluation(One tests, BoundDagEvaluation e1, [NotNullWhen(true)] out BoundDagEvaluation? underlying) + { + if (tests.Test is BoundDagEvaluation eval && + e1.IsEquivalentTo(eval) && + IsSameEntity(eval.Input, e1.Input)) + { + Debug.Assert(!eval.Input.Equals(e1.Input)); + underlying = eval; + return true; + } + + underlying = null; + return false; + } + + private static Tests RemoveAssignmentEvaluation(One tests, BoundDagAssignmentEvaluation e1) + { + if (IsEquivalentEvaluation(tests, e1, out _)) + { + return Tests.True.Instance; + } + + return tests; + } + + private static Tests RemoveSimpleEvaluationWithResultTemp(One tests, ref ImmutableDictionary tempMap, BoundDagEvaluation e1) + { + if (IsEquivalentEvaluation(tests, e1, out var eval)) + { + // Refer to the result of e1 instead of result of eval, this will allow reusing results of evaluations that might be coming next + AddResultTempReplacement(ref tempMap, eval, e1); + return Tests.True.Instance; + } + + return tests; + } + + private static void AddResultTempReplacement(ref ImmutableDictionary tempMap, BoundDagEvaluation oldEval, BoundDagEvaluation newEval) + { + AddTempReplacement(ref tempMap, oldEval.MakeResultTemp(), newEval.MakeResultTemp()); + } + + private static void AddTempReplacement(ref ImmutableDictionary tempMap, BoundDagTemp oldTemp, BoundDagTemp newTemp) + { + var current = newTemp; + + do + { + if (current.Equals(oldTemp)) + { + throw ExceptionUtilities.Unreachable(); + } + } + while (tempMap.TryGetValue(current, out current)); + + Debug.Assert(oldTemp.Type.Equals(newTemp.Type, TypeCompareKind.AllIgnoreOptions)); + tempMap = tempMap.SetItem(oldTemp, newTemp); + } + + private static Tests RemoveDeconstructEvaluation(One tests, ref ImmutableDictionary tempMap, BoundDagDeconstructEvaluation e1) + { + if (IsEquivalentEvaluation(tests, e1, out var eval)) + { + // Refer to the result of e1 instead of result of eval, this will allow reusing results of evaluations that might be coming next + AddOutParameterTempsReplacement(ref tempMap, (BoundDagDeconstructEvaluation)eval, e1); + return Tests.True.Instance; + } + + return tests; + } + + private static void AddOutParameterTempsReplacement(ref ImmutableDictionary tempMap, BoundDagDeconstructEvaluation oldDeconstruct, BoundDagDeconstructEvaluation newDeconstruct) + { + ArrayBuilder newOutParamTemps = newDeconstruct.MakeOutParameterTemps(); + ArrayBuilder oldOutParamTemps = oldDeconstruct.MakeOutParameterTemps(); + for (int i = 0; i < oldOutParamTemps.Count; i++) + { + AddTempReplacement(ref tempMap, oldOutParamTemps[i], newOutParamTemps[i]); + } + newOutParamTemps.Free(); + oldOutParamTemps.Free(); + } + + private static Tests RemoveTypeEvaluation(One tests, DecisionDagBuilder dagBuilder, ref ImmutableDictionary tempMap, BoundDagTypeEvaluation e1) + { + if (tests.Test is BoundDagTypeEvaluation typeEval && IsSameEntity(typeEval.MakeResultTemp(), e1.MakeResultTemp())) + { + Debug.Assert(!typeEval.Equals(e1)); + + if (e1.Type.Equals(typeEval.Type, TypeCompareKind.AllIgnoreOptions)) + { + // Refer to the result of e1 instead of result of typeEval, this will allow reusing results of evaluations that might be coming next + AddResultTempReplacement(ref tempMap, typeEval, e1); + return Tests.True.Instance; + } + else if (typeEval.Input is { Source: not BoundDagTypeEvaluation } typeEvalInput) + { + BoundDagTemp e1Input = NotTypeEvaluationInput(e1); + + if (!typeEvalInput.Equals(e1Input)) + { + if (IsUnionTryGetValueValue(typeEvalInput, out _)) + { + if (IsUnionTryGetValueValue(e1Input, out _)) + { + if (!typeEvalInput.Type.Equals(e1Input.Type, TypeCompareKind.AllIgnoreOptions)) + { + return replaceUnionTypeEvaluation(dagBuilder, e1, typeEval) ?? tests; + } + } + else if (IsUnionValue(e1Input, out _)) + { + return replaceUnionTypeEvaluation(dagBuilder, e1, typeEval) ?? tests; + } + } + else if (IsUnionValue(typeEvalInput, out _) && IsUnionTryGetValueValue(e1Input, out _)) + { + return replaceUnionTypeEvaluation(dagBuilder, e1, typeEval) ?? tests; + } + } + } + } + + return tests; + + static Tests? replaceUnionTypeEvaluation(DecisionDagBuilder dagBuilder, BoundDagTypeEvaluation e1, BoundDagTypeEvaluation typeEval) + { + bool trueTestPermitsTrueOther = true; + bool falseTestPermitsTrueOther = true; + bool trueTestImpliesTrueOther = false; + bool falseTestImpliesTrueOther = false; + dagBuilder.CheckConsistentTypeTestsDecision(null, typeEval.Syntax, ref trueTestPermitsTrueOther, ref falseTestPermitsTrueOther, ref trueTestImpliesTrueOther, ref falseTestImpliesTrueOther, e1.Type, typeEval.Type); + + if (trueTestImpliesTrueOther) // Only then there is a guarantee that the type check won't be necessary and the second TryGetValue call can be and will be completely omitted + { + // Change typeEval to use input of e1 instead, this will allow us to avoid calling a different Union API to get this value + var newTypeEval = typeEval.Update(e1.Input); + + BoundDagTemp oldTemp = typeEval.MakeResultTemp(); + BoundDagTemp newTemp = newTypeEval.MakeResultTemp(); + + if (newTemp.Equals(oldTemp)) + { + // The difference between the two evaluations is in the specific Union API used to extract the value + // from Union instance. However, given our equality rules they are equal, which might result in state + // unification ignoring the API choice that we just made. To prevent that undesired unification, + // we inject BoundDagPassThroughEvaluation right after the updated evaluation. The sole purpose of + // BoundDagPassThroughEvaluation is to prevent state unification until after the specific evaluation + // is performed. Semantically, BoundDagPassThroughEvaluation doesn't calculate a new value, + // it just passes the input through and its result temp is the input temp. + return AndSequence.Create(new Tests.One(newTypeEval), new Tests.One(new BoundDagPassThroughEvaluation(typeEval.Syntax, newTemp))); + } + + Debug.Fail("Do not expect to get in a situation when a type evaluation removal initiates a temp rewrite."); + } + + return null; + } + } + + private static Tests RemoveIndexerEvaluation(One tests, DecisionDagBuilder dagBuilder, DagState state, ref ImmutableDictionary tempMap, BoundDagIndexerEvaluation s1, out Tests? condition) + { + if (tests.Test is BoundDagIndexerEvaluation s2 && s2.IndexerType.Equals(s1.IndexerType, TypeCompareKind.AllIgnoreOptions)) + { + // Even though the two tests appear unrelated (with different inputs), + // it is possible that they are in fact related under certain conditions. + // For instance, the inputs [0] and [^1] point to the same element when remainingTestsLength is 1. + + // Take the top-level input and normalize indices to account for indexer accesses inside a slice. + // For instance [0] in nested list pattern [ 0, ..[$$], 2 ] refers to [1] in the containing list. + (BoundDagTemp s1Input, BoundDagTemp s1LengthTemp, int s1Index) = GetCanonicalInput(s1); + (BoundDagTemp s2Input, BoundDagTemp s2LengthTemp, int s2Index) = GetCanonicalInput(s2); + + if (IsSameEntity(s1Input, s2Input)) + { + Debug.Assert(s1LengthTemp.IsEquivalentTo(s2LengthTemp)); + if (s1Index == s2Index) + { + AddResultTempReplacement(ref tempMap, s2, s1); + condition = null; + return Tests.True.Instance; + } + + if (s1Index < 0 != s2Index < 0) + { + Debug.Assert(state.RemainingValues.ContainsKey(s1LengthTemp)); + var lengthValues = (IConstantValueSet)state.RemainingValues[s1LengthTemp]; + // We do not expect an empty set here because an indexer evaluation is always preceded by + // a length test of which an impossible match would have made the rest of the tests unreachable. + Debug.Assert(!lengthValues.IsEmpty); + + // Compute the length value that would make these two indices point to the same element. + int lengthValue = s1Index < 0 ? s2Index - s1Index : s1Index - s2Index; + if (lengthValues.All(BinaryOperatorKind.Equal, lengthValue)) + { + // If the length is known to be exact, the two are considered to point to the same element. + AddResultTempReplacement(ref tempMap, s2, s1); + condition = null; + return Tests.True.Instance; + } + + if (!dagBuilder._forLowering && lengthValues.Any(BinaryOperatorKind.Equal, lengthValue)) + { + dagBuilder._suitableForLowering = false; + + // Otherwise, we add a test to make the result conditional on the length value. + condition = new Tests.One(new BoundDagValueTest(s2.Syntax, ConstantValue.Create(lengthValue), s1LengthTemp)); + AddResultTempReplacement(ref tempMap, s2, s1); + return Tests.True.Instance; + } + } + } + } + + condition = null; + return tests; + } + + private static BoundDagTest UpdateDagTempReferences(BoundDagTest test, ref ImmutableDictionary tempMap) + { + switch (test) + { + case BoundDagEvaluation eval: + { + switch (eval) + { + case BoundDagPassThroughEvaluation passThrough: + { + Debug.Assert(passThrough.Input.Source is BoundDagTypeEvaluation); + return new BoundDagPassThroughEvaluation(passThrough.Syntax, ((BoundDagTypeEvaluation)UpdateDagTempReferences((BoundDagTypeEvaluation)passThrough.Input.Source, ref tempMap)).MakeResultTemp()); + } + case BoundDagIndexEvaluation: + case BoundDagTypeEvaluation: + case BoundDagFieldEvaluation: + case BoundDagPropertyEvaluation: + { + if (!TryGetTempReplacement(tempMap, eval.Input, out BoundDagTemp? replacement)) + { + return eval; + } + + var updated = eval.Update(replacement); + AddResultTempReplacement(ref tempMap, eval, updated); + return updated; + } + + case BoundDagIndexerEvaluation indexer: + { + if (!TryGetTempReplacement(tempMap, indexer.Input, out BoundDagTemp? inputReplacement)) + { + return eval; + } + + if (!TryGetTempReplacement(tempMap, indexer.LengthTemp, out BoundDagTemp? lengthReplacement)) + { + lengthReplacement = indexer.LengthTemp; + } + + var indexerEvaluation = indexer.Update(lengthReplacement, inputReplacement); + AddResultTempReplacement(ref tempMap, indexer, indexerEvaluation); + return indexerEvaluation; + } + case BoundDagSliceEvaluation slice: + { + if (!TryGetTempReplacement(tempMap, slice.Input, out BoundDagTemp? inputReplacement)) + { + return eval; + } + + if (!TryGetTempReplacement(tempMap, slice.LengthTemp, out BoundDagTemp? lengthReplacement)) + { + lengthReplacement = slice.LengthTemp; + } + + var sliceEvaluation = slice.Update(lengthReplacement, inputReplacement); + AddResultTempReplacement(ref tempMap, slice, sliceEvaluation); + return sliceEvaluation; + } + case BoundDagAssignmentEvaluation assignment: + { + if (!TryGetTempReplacement(tempMap, assignment.Input, out BoundDagTemp? inputReplacement)) + { + return test; + } + + var assignmentEvaluation = assignment.Update(inputReplacement); + return assignmentEvaluation; + } + case BoundDagDeconstructEvaluation deconstruct: + { + if (!TryGetTempReplacement(tempMap, deconstruct.Input, out BoundDagTemp? replacement)) + { + return test; + } + + var deconstructEvaluation = deconstruct.Update(replacement); + AddOutParameterTempsReplacement(ref tempMap, deconstruct, deconstructEvaluation); + return deconstructEvaluation; + } + + default: + throw ExceptionUtilities.UnexpectedValue(test); + } + } + + case BoundDagRelationalTest: + case BoundDagTypeTest: + case BoundDagNonNullTest: + case BoundDagExplicitNullTest: + case BoundDagValueTest: + { + Debug.Assert(test is not BoundDagEvaluation); + if (!TryGetTempReplacement(tempMap, test.Input, out BoundDagTemp? replacement)) + { + return test; + } + + return test.Update(replacement); + } + + default: + throw ExceptionUtilities.UnexpectedValue(test); + } + } } public sealed class Not : Tests @@ -2340,9 +3995,17 @@ private static ArrayBuilder NegateSequenceElements(ImmutableArray return builder; } - public override Tests RemoveEvaluation(BoundDagEvaluation e) => Create(Negated.RemoveEvaluation(e)); + protected override RemoveEvaluationAndUpdateTempReferencesResult RemoveEvaluationAndUpdateTempReferencesCore(DecisionDagBuilder builder, DagState state, ImmutableArray bindings, ImmutableDictionary tempMap, BoundDagEvaluation e) + { + return new RemoveEvaluationAndUpdateTempReferencesResult( + Create(Negated.RemoveEvaluationAndUpdateTempReferences(builder, state, bindings, tempMap, e)), + tempMap, + conditionToUseFinalResult: null, + tempsUpdatedResult: null); + } + public override Tests RewriteNestedLengthTests() => Create(Negated.RewriteNestedLengthTests()); - public override BoundDagTest ComputeSelectedTest() => Negated.ComputeSelectedTest(); + public override BoundDagTest ComputeSelectedTest(bool forLowering, ref bool suitableForLowering) => Negated.ComputeSelectedTest(forLowering, ref suitableForLowering); public override string Dump(Func dump) => $"Not ({Negated.Dump(dump)})"; public override void Filter( DecisionDagBuilder builder, @@ -2447,40 +4110,94 @@ static void assemble(SequenceTests toAssemble, ArrayBuilder tests) } } - public sealed override Tests RemoveEvaluation(BoundDagEvaluation e) + private enum ReassembleKind { - var testsToRewrite = ArrayBuilder.GetInstance(); - var testsToAssemble = ArrayBuilder.GetInstance(); + And, + Or, + } + + protected sealed override RemoveEvaluationAndUpdateTempReferencesResult RemoveEvaluationAndUpdateTempReferencesCore(DecisionDagBuilder dagBuilder, DagState state, ImmutableArray bindings, ImmutableDictionary tempMap, BoundDagEvaluation e) + { + var testsToRewrite = ArrayBuilder<(Tests? Tests, bool SkipRewrite)>.GetInstance(); + var testsToAssemble = ArrayBuilder<( + ReassembleKind Kind, + int ChildCount, + ImmutableDictionary TempMapToRestore + )>.GetInstance(); var testsRewritten = ArrayBuilder.GetInstance(); - testsToRewrite.Push(this); + testsToRewrite.Push((this, SkipRewrite: false)); do { - var current = testsToRewrite.Pop(); + var (current, skipRewrite) = testsToRewrite.Pop(); switch (current) { case SequenceTests seq: - testsToAssemble.Push(seq); - testsToRewrite.Push(null); // marker to indicate we need to reassemble after handling children - testsToRewrite.AddRange(seq.RemainingTests!); + ImmutableArray remainingTests = seq.RemainingTests; + testsToAssemble.Push((seq is AndSequence ? ReassembleKind.And : ReassembleKind.Or, remainingTests.Length, tempMap)); + testsToRewrite.Push((null, false)); // marker to indicate we need to reassemble after handling children + + // Push in reverse order to rewrite in original order + for (int i = remainingTests.Length - 1; i >= 0; i--) + { + testsToRewrite.Push((seq.RemainingTests[i], SkipRewrite: false)); + } break; case null: - var toAssemble = testsToAssemble.Pop(); - var length = toAssemble.RemainingTests.Length; - var newSequence = ArrayBuilder.GetInstance(length); - for (int i = 0; i < length; i++) { - newSequence.Add(testsRewritten.Pop()); + var (kind, childCount, tempMapToRestore) = testsToAssemble.Pop(); + var newSequence = ArrayBuilder.GetInstance(childCount); + newSequence.Count = childCount; + for (int i = childCount - 1; i >= 0; i--) + { + newSequence[i] = testsRewritten.Pop(); + } + + if (kind is ReassembleKind.And) + { + AddBindingsPatchingAssignments(bindings, tempMapToRestore, tempMap, newSequence); + } + else + { + Debug.Assert(tempMapToRestore == tempMap); + } + + testsRewritten.Push(kind is ReassembleKind.And ? AndSequence.Create(newSequence) : OrSequence.Create(newSequence)); + tempMap = tempMapToRestore; } - - testsRewritten.Push(toAssemble.Update(newSequence)); break; default: - testsRewritten.Push(current.RemoveEvaluation(e)); + { + Debug.Assert(testsToAssemble.Count != 0); // If we have a child to rewrite, we must have a parent to reassemble. + + if (skipRewrite) + { + testsRewritten.Push(current); + break; + } + + if (testsToAssemble.Peek().Kind == ReassembleKind.And) + { + RemoveEvaluationAndUpdateTempReferencesResult rewriteResult = current.RemoveEvaluationAndUpdateTempReferencesCore(dagBuilder, state, bindings, tempMap, e); + + if (rewriteResult.FinalTempMap == tempMap && rewriteResult.ConditionToUseFinalResult is null) + { + testsRewritten.Push(rewriteResult.FinalResult); + } + else + { + handleComplexResultOfChildRewriteInAndSequence(rewriteResult); + } + } + else + { + testsRewritten.Push(current.RemoveEvaluationAndUpdateTempReferences(dagBuilder, state, bindings, tempMap, e)); + } + } break; } } @@ -2497,7 +4214,131 @@ public sealed override Tests RemoveEvaluation(BoundDagEvaluation e) testsToAssemble.Free(); testsRewritten.Free(); - return result; + return new RemoveEvaluationAndUpdateTempReferencesResult(result, tempMap, conditionToUseFinalResult: null, tempsUpdatedResult: null); + + void handleComplexResultOfChildRewriteInAndSequence(RemoveEvaluationAndUpdateTempReferencesResult rewriteResult) + { + Debug.Assert(testsToAssemble.Peek().Kind == ReassembleKind.And); + + if (rewriteResult.ConditionToUseFinalResult is null) + { + // Trivial case. Keep rewriting with updated temp map and patchup bindings while reassembling the AndSequence + testsRewritten.Push(rewriteResult.FinalResult); + tempMap = rewriteResult.FinalTempMap; + return; + } + + Debug.Assert(rewriteResult.TempsUpdatedResult is not null); + + var (_, childCount, tempMapToRestore) = testsToAssemble.Peek(); + + var leftToRewriteBuilder = ArrayBuilder.GetInstance(); + popAndAddChildrenLeftToRewrite(leftToRewriteBuilder); + Debug.Assert(leftToRewriteBuilder.Count < childCount); + + // + // We need to complete the sequence with a test of the following shape: + // + // OrSequence( + // AndSequence( + // ConditionToUseFinalResult, + // FinalResult, + // < remaining original nodes from the sequence after evaluation is removed in them and locals are updated by using FinalTempMap > + // ), + // AndSequence( + // Not (ConditionToUseFinalResult), + // TempsUpdatedResult, + // < remaining original nodes from the sequence after evaluation is removed in them and locals are updated by using the current tempMap > + // ) + // ) + // + + // Patach the count of children in the pending AndSequence + testsToAssemble[^1] = (ReassembleKind.And, childCount - leftToRewriteBuilder.Count, tempMapToRestore); + pushConditionalResult(rewriteResult, leftToRewriteBuilder); + leftToRewriteBuilder.Free(); + } + + void pushConditionalResult(RemoveEvaluationAndUpdateTempReferencesResult rewriteResult, ArrayBuilder leftToRewriteBuilder) + { + Debug.Assert(rewriteResult.ConditionToUseFinalResult is not null); + Debug.Assert(rewriteResult.TempsUpdatedResult is not null); + // + // We need "to push" a test of the following shape: + // + // OrSequence( + // AndSequence( + // ConditionToUseFinalResult, + // FinalResult, + // < remaining original nodes from the sequence after evaluation is removed in them and locals are updated by using FinalTempMap > + // ), + // AndSequence( + // Not (ConditionToUseFinalResult), + // TempsUpdatedResult, + // < remaining original nodes from the sequence after evaluation is removed in them and locals are updated by using the current tempMap > + // ) + // ) + // + + testsToAssemble.Add((ReassembleKind.Or, 2, tempMap)); + testsToRewrite.Push((null, false)); // marker to indicate we need to reassemble after handling children + + // Children are pushed into testsToRewrite in reverse order + + // AndSequence( + // Not (ConditionToUseFinalResult), + // TempsUpdatedResult, + // < remaining original nodes from the sequence after evaluation is removed in them and locals are updated by using the current tempMap > + // ) + + int leftToRewrite = leftToRewriteBuilder.Count; + testsToAssemble.Add((ReassembleKind.And, leftToRewrite + 2, tempMap)); + testsToRewrite.Push((null, false)); // marker to indicate we need to reassemble after handling children + + for (int i = leftToRewrite - 1; i >= 0; i--) + { + testsToRewrite.Push((leftToRewriteBuilder![i], SkipRewrite: false)); + } + + testsToRewrite.Push((rewriteResult.TempsUpdatedResult, SkipRewrite: true)); + testsToRewrite.Push((Not.Create(rewriteResult.ConditionToUseFinalResult), SkipRewrite: true)); + + // AndSequence( + // ConditionToUseFinalResult, + // FinalResult, + // < remaining original nodes from the sequence after evaluation is removed in them and locals are updated by using FinalTempMap > + // ) + + testsToAssemble.Add((ReassembleKind.And, leftToRewrite + 2, TempMapToRestore: tempMap)); + testsToRewrite.Push((null, false)); // marker to indicate we need to reassemble after handling children + + for (int i = leftToRewrite - 1; i >= 0; i--) + { + testsToRewrite.Push((leftToRewriteBuilder![i], SkipRewrite: false)); + } + + testsToRewrite.Push((rewriteResult.FinalResult, SkipRewrite: true)); + testsToRewrite.Push((rewriteResult.ConditionToUseFinalResult, SkipRewrite: true)); + + tempMap = rewriteResult.FinalTempMap; + } + + void popAndAddChildrenLeftToRewrite(ArrayBuilder leftToRewriteBuilder) + { + while (true) + { + var (toRewrite, skip) = testsToRewrite.Peek(); + if (toRewrite is null) + { + break; + } + + Debug.Assert(!skip); + Debug.Assert(!skip); + leftToRewriteBuilder.Add(toRewrite); + testsToRewrite.Pop(); + } + } } public sealed override Tests RewriteNestedLengthTests() @@ -2683,14 +4524,14 @@ public sealed override int GetHashCode() } } - public sealed override BoundDagTest ComputeSelectedTest() + public sealed override BoundDagTest ComputeSelectedTest(bool forLowering, ref bool suitableForLowering) { Tests firstTest; var current = this; while (true) { - if (current.ComputeSelectedTestEasyOut() is { } easy) + if (current.ComputeSelectedTestEasyOut(forLowering, ref suitableForLowering) is { } easy) { return easy; } @@ -2705,10 +4546,10 @@ public sealed override BoundDagTest ComputeSelectedTest() current = sequence; } - return firstTest.ComputeSelectedTest(); + return firstTest.ComputeSelectedTest(forLowering, ref suitableForLowering); } - protected virtual BoundDagTest? ComputeSelectedTestEasyOut() => null; + protected virtual BoundDagTest? ComputeSelectedTestEasyOut(bool forLowering, ref bool suitableForLowering) => null; } /// @@ -2717,7 +4558,10 @@ public sealed override BoundDagTest ComputeSelectedTest() /// public sealed class AndSequence : SequenceTests { - private AndSequence(ImmutableArray remainingTests) : base(remainingTests) { } + private AndSequence(ImmutableArray remainingTests) : base(remainingTests) + { + Debug.Assert(!remainingTests.Any(t => t is AndSequence)); + } public override Tests Update(ArrayBuilder remainingTests) => Create(remainingTests); public static Tests Create(Tests t1, Tests t2) { @@ -2758,12 +4602,36 @@ public static Tests Create(ArrayBuilder remainingTests) remainingTests.Free(); return result; } - protected override BoundDagTest? ComputeSelectedTestEasyOut() + protected override BoundDagTest? ComputeSelectedTestEasyOut(bool forLowering, ref bool suitableForLowering) { // Our simple heuristic is to perform the first test of the // first possible matched case, with two exceptions. if (RemainingTests[0] is One { Test: { Kind: BoundKind.DagNonNullTest } planA }) + { + BoundDagTest? easyOutForLowering = tryGetEasyOut(planA); + + if (easyOutForLowering is not null) + { + if (easyOutForLowering != (object)planA && !forLowering && ValueSetFactory.TypeUnionValueSetFactoryForInput(planA.Input) is not null) + { + // We need a test about `null` present in the Dag to properly handle exhaustiveness + // analysis for 'null' values when we are matching for a union of types. + // We don't know if an explicit test about 'null' is coming and haven't yet matched + // against 'null', otherwise the test would be filtered out. + // Therefore, we prefer selecting this test. + + suitableForLowering = false; + return planA; + } + + return easyOutForLowering; + } + } + + return null; + + BoundDagTest? tryGetEasyOut(BoundDagTest planA) { switch (RemainingTests[1]) { @@ -2771,18 +4639,18 @@ public static Tests Create(ArrayBuilder remainingTests) // null check and perform the type test directly. That's because the type test // has the side-effect of performing the null check for us. case One { Test: { Kind: BoundKind.DagTypeTest } planB1 }: - return (planA.Input == planB1.Input) ? planB1 : planA; + return planA.Input.Equals(planB1.Input) ? planB1 : planA; // In the specific case of a null check following by a value test (which occurs for // pattern matching a string constant pattern), we skip the // null check and perform the value test directly. That's because the value test // has the side-effect of performing the null check for us. case One { Test: { Kind: BoundKind.DagValueTest } planB2 }: - return (planA.Input == planB2.Input) ? planB2 : planA; + return planA.Input.Equals(planB2.Input) ? planB2 : planA; } - } - return null; + return null; + } } public override string Dump(Func dump) { @@ -2796,7 +4664,10 @@ public override string Dump(Func dump) /// public sealed class OrSequence : SequenceTests { - private OrSequence(ImmutableArray remainingTests) : base(remainingTests) { } + private OrSequence(ImmutableArray remainingTests) : base(remainingTests) + { + Debug.Assert(!remainingTests.Any(t => t is OrSequence)); + } public override Tests Update(ArrayBuilder remainingTests) => Create(remainingTests); public static Tests Create(Tests t1, Tests t2) { diff --git a/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder_CheckOrReachability.cs b/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder_CheckOrReachability.cs index 2e667aa537d7..20316ac136e0 100644 --- a/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder_CheckOrReachability.cs +++ b/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder_CheckOrReachability.cs @@ -62,6 +62,7 @@ internal static void CheckRedundantPatternsForIsPattern( SyntaxNode syntax, BoundExpression inputExpression, BoundPattern pattern, + bool hasUnionMatching, BindingDiagnosticBag diagnostics) { if (pattern.HasErrors) @@ -75,7 +76,7 @@ internal static void CheckRedundantPatternsForIsPattern( var redundantNodes = PooledHashSet.GetInstance(); var noPreviousCases = ArrayBuilder.GetInstance(0); - CheckOrAndAndReachability(noPreviousCases, patternIndex: 0, pattern: pattern, builder: builder, rootIdentifier: rootIdentifier, syntax: syntax, diagnostics: diagnostics, redundantNodes); + CheckOrAndAndReachability(noPreviousCases, patternIndex: 0, pattern: pattern, hasUnionMatching: hasUnionMatching, builder: builder, rootIdentifier: rootIdentifier, syntax: syntax, diagnostics: diagnostics, redundantNodes); ReportRedundant(redundantNodes, diagnostics); redundantNodes.Free(); @@ -126,12 +127,13 @@ static void checkRedundantPatternsForSwitchExpression( return; } - existingCases.Add(builder.MakeTestsForPattern(++index, switchArm.Syntax, rootIdentifier, switchArm.Pattern, whenClause: switchArm.WhenClause, label: switchArm.Label)); + existingCases.Add(builder.MakeTestsForPattern(++index, switchArm.Syntax, rootIdentifier, switchArm.Pattern, switchArm.HasUnionMatching, whenClause: switchArm.WhenClause, label: switchArm.Label)); } for (int patternIndex = 0; patternIndex < switchArms.Length; patternIndex++) { - CheckOrAndAndReachability(existingCases, patternIndex, switchArms[patternIndex].Pattern, builder, rootIdentifier, syntax, diagnostics, redundantNodes); + BoundSwitchExpressionArm switchArm = switchArms[patternIndex]; + CheckOrAndAndReachability(existingCases, patternIndex, switchArm.Pattern, switchArm.HasUnionMatching, builder, rootIdentifier, syntax, diagnostics, redundantNodes); } ReportRedundant(redundantNodes, diagnostics); @@ -180,7 +182,7 @@ static void checkRedundantPatternsForSwitchStatement( return; } - existingCases.Add(builder.MakeTestsForPattern(++index, label.Syntax, rootIdentifier, label.Pattern, label.WhenClause, label.Label)); + existingCases.Add(builder.MakeTestsForPattern(++index, label.Syntax, rootIdentifier, label.Pattern, label.HasUnionMatching, label.WhenClause, label.Label)); } } } @@ -192,7 +194,7 @@ static void checkRedundantPatternsForSwitchStatement( { if (label.Syntax.Kind() != SyntaxKind.DefaultSwitchLabel) { - CheckOrAndAndReachability(existingCases, patternIndex, label.Pattern, builder, rootIdentifier, syntax, diagnostics, redundantNodes); + CheckOrAndAndReachability(existingCases, patternIndex, label.Pattern, label.HasUnionMatching, builder, rootIdentifier, syntax, diagnostics, redundantNodes); patternIndex++; } } @@ -336,6 +338,7 @@ private static void CheckOrAndAndReachability( ArrayBuilder previousCases, int patternIndex, BoundPattern pattern, + bool hasUnionMatching, DecisionDagBuilder builder, BoundDagTemp rootIdentifier, SyntaxNode syntax, @@ -346,6 +349,11 @@ private static void CheckOrAndAndReachability( try { + if (hasUnionMatching) + { + pattern = UnionMatchingRewriter.Rewrite(builder._compilation, pattern); + } + var normalizedPattern = PatternNormalizer.Rewrite(pattern, rootIdentifier.Type); #if ROSLYN_TEST_REDUNDANT_PATTERN @@ -359,7 +367,7 @@ private static void CheckOrAndAndReachability( // but our method for detecting redundancies only detects those in `or` cases (which we bring to the top after normalization). // By analyzing `not A` too we can report more redundancies. // For example: `if (o is not ())`, `if (i is 42 and not 43)` (which is the negation of `not 42 or 43`) - var negated = new BoundNegatedPattern(pattern.Syntax, negated: pattern, pattern.InputType, narrowedType: pattern.InputType); + var negated = new BoundNegatedPattern(pattern.Syntax, negated: pattern, isUnionMatching: false, pattern.InputType, narrowedType: pattern.InputType); var normalizedNegatedPattern = PatternNormalizer.Rewrite(negated, rootIdentifier.Type); #if ROSLYN_TEST_REDUNDANT_PATTERN @@ -403,7 +411,7 @@ static void populateStateForCases(ArrayBuilder set, PooledHashSet< } Debug.Assert(diagSyntax is not null); - casesBuilder.Add(context.Builder.MakeTestsForPattern(++index, diagSyntax, context.RootIdentifier, pattern, whenClause: null, label: label)); + casesBuilder.Add(context.Builder.MakeTestsForPattern(++index, diagSyntax, context.RootIdentifier, pattern, hasUnionMatching: false, whenClause: null, label: label)); } } @@ -787,6 +795,7 @@ static TypeSymbol narrowedTypeForBinary(BoundPattern resultLeft, BoundPattern re public override BoundNode? Visit(BoundNode? node) { + Debug.Assert(node is not BoundPattern { IsUnionMatching: true }); Debug.Assert(node is BoundBinaryPattern or BoundRecursivePattern or BoundListPattern @@ -928,7 +937,7 @@ public BoundPattern NegateIfNeeded(BoundPattern node) return negated; } - var result = new BoundNegatedPattern(node.Syntax, node, node.InputType, narrowedType: node.InputType); + var result = new BoundNegatedPattern(node.Syntax, node, isUnionMatching: false, node.InputType, narrowedType: node.InputType); if (node.WasCompilerGenerated) { result.MakeCompilerGenerated(); @@ -976,7 +985,7 @@ private static BoundPattern WithInputTypeCheckIfNeeded(BoundPattern pattern, Typ if (pattern is BoundTypePattern typePattern1) { - return typePattern1.Update(typePattern1.DeclaredType, typePattern1.IsExplicitNotNullTest, inputType, typePattern1.NarrowedType); + return typePattern1.Update(typePattern1.DeclaredType, typePattern1.IsExplicitNotNullTest, isUnionMatching: false, inputType, typePattern1.NarrowedType); } if (pattern is BoundRecursivePattern recursivePattern) @@ -987,7 +996,7 @@ private static BoundPattern WithInputTypeCheckIfNeeded(BoundPattern pattern, Typ recursivePattern.DeconstructMethod, recursivePattern.Deconstruction, recursivePattern.Properties, recursivePattern.IsExplicitNotNullTest, recursivePattern.Variable, recursivePattern.VariableAccess, - inputType, recursivePattern.NarrowedType); + isUnionMatching: false, inputType, recursivePattern.NarrowedType); } if (pattern is BoundDiscardPattern discardPattern) @@ -999,25 +1008,25 @@ private static BoundPattern WithInputTypeCheckIfNeeded(BoundPattern pattern, Typ { return negatedPattern.Update( WithInputTypeCheckIfNeeded(negatedPattern.Negated, inputType), - inputType, inputType); + isUnionMatching: false, inputType, inputType); } if (pattern is BoundConstantPattern constantPattern) { var narrowedType = constantPattern.ConstantValue.IsNull ? inputType : constantPattern.NarrowedType; - return constantPattern.Update(constantPattern.Value, constantPattern.ConstantValue, inputType, narrowedType); + return constantPattern.Update(constantPattern.Value, constantPattern.ConstantValue, isUnionMatching: false, inputType, narrowedType); } if (pattern is BoundRelationalPattern relationalPattern) { - return relationalPattern.Update(relationalPattern.Relation, relationalPattern.Value, relationalPattern.ConstantValue, inputType, relationalPattern.NarrowedType); + return relationalPattern.Update(relationalPattern.Relation, relationalPattern.Value, relationalPattern.ConstantValue, isUnionMatching: false, inputType, relationalPattern.NarrowedType); } if (pattern is BoundDeclarationPattern declarationPattern) { // We drop the variable symbol and access to avoid input type mismtaches, resulting in a designation discard return declarationPattern.Update(declarationPattern.DeclaredType, declarationPattern.IsVar, - variable: null, variableAccess: null, inputType, declarationPattern.NarrowedType); + variable: null, variableAccess: null, isUnionMatching: false, inputType, declarationPattern.NarrowedType); } Debug.Assert(pattern is BoundITuplePattern or BoundListPattern); @@ -1026,7 +1035,7 @@ private static BoundPattern WithInputTypeCheckIfNeeded(BoundPattern pattern, Typ BoundPattern typePattern = new BoundTypePattern(pattern.Syntax, new BoundTypeExpression(pattern.Syntax, aliasOpt: null, pattern.InputType), - isExplicitNotNullTest: false, inputType, narrowedType: pattern.InputType).MakeCompilerGenerated(); + isExplicitNotNullTest: false, isUnionMatching: false, inputType, narrowedType: pattern.InputType).MakeCompilerGenerated(); var result = new BoundBinaryPattern(pattern.Syntax, disjunction: false, left: typePattern, right: pattern, inputType, pattern.NarrowedType); @@ -1040,7 +1049,7 @@ private static BoundPattern WithInputTypeCheckIfNeeded(BoundPattern pattern, Typ public override BoundNode? VisitDeclarationPattern(BoundDeclarationPattern node) { - var result = new BoundDeclarationPattern(node.Syntax, node.DeclaredType, node.IsVar, node.Variable, node.VariableAccess, node.InputType, node.NarrowedType) + var result = new BoundDeclarationPattern(node.Syntax, node.DeclaredType, node.IsVar, node.Variable, node.VariableAccess, isUnionMatching: false, node.InputType, node.NarrowedType) .MakeCompilerGenerated(); TryPushOperand(NegateIfNeeded(result)); return null; @@ -1080,21 +1089,21 @@ private static BoundPattern WithInputTypeCheckIfNeeded(BoundPattern pattern, Typ if (node.DeclaredType is not null) { // `Type` - initialCheck = new BoundTypePattern(node.Syntax, node.DeclaredType, node.IsExplicitNotNullTest, node.InputType, node.NarrowedType, node.HasErrors); + initialCheck = new BoundTypePattern(node.Syntax, node.DeclaredType, node.IsExplicitNotNullTest, isUnionMatching: false, node.InputType, node.NarrowedType, node.HasErrors); } else if (node.InputType.CanContainNull()) { // `not null` var nullCheck = new BoundConstantPattern(node.Syntax, new BoundLiteral(node.Syntax, constantValueOpt: ConstantValue.Null, type: node.InputType, hasErrors: false), - ConstantValue.Null, node.InputType, node.InputType, hasErrors: false); - initialCheck = new BoundNegatedPattern(node.Syntax, nullCheck, node.InputType, narrowedType: node.InputType); + ConstantValue.Null, isUnionMatching: false, node.InputType, node.InputType, hasErrors: false); + initialCheck = new BoundNegatedPattern(node.Syntax, nullCheck, isUnionMatching: false, node.InputType, narrowedType: node.InputType); } else { // `{ }` initialCheck = new BoundRecursivePattern(node.Syntax, declaredType: null, deconstructMethod: null, deconstruction: default, - ImmutableArray.Empty, isExplicitNotNullTest: false, variable: null, variableAccess: null, node.InputType, node.InputType); + ImmutableArray.Empty, isExplicitNotNullTest: false, variable: null, variableAccess: null, isUnionMatching: false, node.InputType, node.InputType); } TryPushOperand(NegateIfNeeded(initialCheck)); Debug.Assert(_evalSequence.Count == startOfLeft + 1); @@ -1120,7 +1129,7 @@ private static BoundPattern WithInputTypeCheckIfNeeded(BoundPattern pattern, Typ newPattern.Syntax, declaredType: node.DeclaredType, deconstructMethod: node.DeconstructMethod, deconstruction: newSubPatterns, properties: default, isExplicitNotNullTest: false, variable: null, variableAccess: null, - node.InputType, node.NarrowedType, node.HasErrors); + isUnionMatching: false, node.InputType, node.NarrowedType, node.HasErrors); if (wasCompilerGenerated) { @@ -1155,7 +1164,7 @@ private static BoundPattern WithInputTypeCheckIfNeeded(BoundPattern pattern, Typ newPattern.Syntax, declaredType: node.DeclaredType, deconstructMethod: null, deconstruction: default, properties: newSubPatterns, isExplicitNotNullTest: false, variable: null, variableAccess: null, - node.InputType, node.NarrowedType, node.HasErrors); + isUnionMatching: false, node.InputType, node.NarrowedType, node.HasErrors); if (wasCompilerGenerated) { @@ -1223,7 +1232,7 @@ private void VisitPatternAndCombine(SyntaxNode syntax, BoundPattern pattern, int // `(_, ..., _)` (effectively a not null and Length test) var lengthTest = new BoundITuplePattern(ituplePattern.Syntax, ituplePattern.GetLengthMethod, ituplePattern.GetItemMethod, discards, - ituplePattern.InputType, ituplePattern.NarrowedType); + isUnionMatching: false, ituplePattern.InputType, ituplePattern.NarrowedType); TryPushOperand(NegateIfNeeded(lengthTest)); Debug.Assert(_evalSequence.Count == startOfLeft + 1); @@ -1240,7 +1249,7 @@ private void VisitPatternAndCombine(SyntaxNode syntax, BoundPattern pattern, int ImmutableArray newSubpatterns = discards.SetItem(i, subpatterns[i].WithPattern(newPattern)); BoundPattern newITuple = new BoundITuplePattern(newPattern.Syntax, ituplePattern.GetLengthMethod, - ituplePattern.GetItemMethod, newSubpatterns, ituplePattern.InputType, ituplePattern.NarrowedType); + ituplePattern.GetItemMethod, newSubpatterns, isUnionMatching: false, ituplePattern.InputType, ituplePattern.NarrowedType); if (wasCompilerGenerated) { @@ -1305,7 +1314,7 @@ private void VisitPatternAndCombine(SyntaxNode syntax, BoundPattern pattern, int BoundPattern newList = new BoundListPattern( newPattern.Syntax, newSubpatterns, hasSlice, listPattern.LengthAccess, listPattern.IndexerAccess, listPattern.ReceiverPlaceholder, listPattern.ArgumentPlaceholder, listPattern.Variable, listPattern.VariableAccess, - listPattern.InputType, listPattern.NarrowedType); + isUnionMatching: false, listPattern.InputType, listPattern.NarrowedType); if (wasCompilerGenerated) { @@ -1336,7 +1345,7 @@ private void VisitPatternAndCombine(SyntaxNode syntax, BoundPattern pattern, int BoundPattern newList = new BoundListPattern( newPattern.Syntax, newSubpatterns, hasSlice: true, listPattern.LengthAccess, listPattern.IndexerAccess, listPattern.ReceiverPlaceholder, listPattern.ArgumentPlaceholder, listPattern.Variable, listPattern.VariableAccess, - listPattern.InputType, listPattern.NarrowedType); + isUnionMatching: false, listPattern.InputType, listPattern.NarrowedType); if (wasCompilerGenerated) { diff --git a/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder_ListPatterns.cs b/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder_ListPatterns.cs index ea073fd9bf83..cd0f0b4d6c2f 100644 --- a/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder_ListPatterns.cs +++ b/src/Compilers/CSharp/Portable/Binder/DecisionDagBuilder_ListPatterns.cs @@ -11,21 +11,24 @@ namespace Microsoft.CodeAnalysis.CSharp { internal sealed partial class DecisionDagBuilder { - private Tests MakeTestsAndBindingsForListPattern(BoundDagTemp input, BoundListPattern list, out BoundDagTemp output, ArrayBuilder bindings) + private Tests MakeTestsAndBindingsForListPattern(TestInputOutputInfo inputInfo, BoundListPattern list, out TestInputOutputInfo outputInfo, ArrayBuilder bindings) { - Debug.Assert(input.Type.IsErrorType() || list.HasErrors || list.InputType.IsErrorType() || - input.Type.Equals(list.InputType, TypeCompareKind.AllIgnoreOptions) && - input.Type.StrippedType().Equals(list.NarrowedType, TypeCompareKind.ConsiderEverything) && +#if DEBUG + TypeSymbol inputType = inputInfo.GetInputType(); + Debug.Assert(inputType.IsErrorType() || list.HasErrors || list.InputType.IsErrorType() || + inputType.Equals(list.InputType, TypeCompareKind.AllIgnoreOptions) && + inputType.StrippedType().Equals(list.NarrowedType, TypeCompareKind.ConsiderEverything) && list.Subpatterns.Count(p => p.Kind == BoundKind.SlicePattern) == (list.HasSlice ? 1 : 0) && list.LengthAccess is not null); - +#endif var syntax = list.Syntax; var subpatterns = list.Subpatterns; var tests = ArrayBuilder.GetInstance(4 + subpatterns.Length * 2); - output = input = MakeConvertToType(input, list.Syntax, list.NarrowedType, isExplicitTest: false, tests); + inputInfo = MakeConvertToType(inputInfo, list.Syntax, list.NarrowedType, isExplicitTest: false, tests); if (list.HasErrors) { + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); tests.Add(new Tests.One(new BoundDagTypeTest(list.Syntax, ErrorType(), input, hasErrors: true))); } else if (list.HasSlice && @@ -36,12 +39,13 @@ private Tests MakeTestsAndBindingsForListPattern(BoundDagTemp input, BoundListPa } else { + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); Debug.Assert(list.LengthAccess is not null); var lengthProperty = Binder.GetPropertySymbol(list.LengthAccess, out _, out _); Debug.Assert(lengthProperty is not null); var lengthEvaluation = new BoundDagPropertyEvaluation(syntax, lengthProperty, isLengthOrCount: true, input); tests.Add(new Tests.One(lengthEvaluation)); - var lengthTemp = new BoundDagTemp(syntax, _compilation.GetSpecialType(SpecialType.System_Int32), lengthEvaluation); + var lengthTemp = lengthEvaluation.MakeResultTemp(); tests.Add(new Tests.One(list.HasSlice ? new BoundDagRelationalTest(syntax, BinaryOperatorKind.IntGreaterThanOrEqual, ConstantValue.Create(subpatterns.Length - 1), lengthTemp) : new BoundDagValueTest(syntax, ConstantValue.Create(subpatterns.Length), lengthTemp))); @@ -65,7 +69,7 @@ private Tests MakeTestsAndBindingsForListPattern(BoundDagTemp input, BoundListPa slice.IndexerAccess, slice.ReceiverPlaceholder, slice.ArgumentPlaceholder, input); tests.Add(new Tests.One(sliceEvaluation)); - var sliceTemp = new BoundDagTemp(slicePattern.Syntax, slicePattern.InputType, sliceEvaluation); + var sliceTemp = sliceEvaluation.MakeResultTemp(); tests.Add(MakeTestsAndBindings(sliceTemp, slicePattern, bindings)); } @@ -80,16 +84,18 @@ private Tests MakeTestsAndBindingsForListPattern(BoundDagTemp input, BoundListPa list.IndexerAccess, list.ReceiverPlaceholder, list.ArgumentPlaceholder, input); tests.Add(new Tests.One(indexEvaluation)); - var indexTemp = new BoundDagTemp(subpattern.Syntax, subpattern.InputType, indexEvaluation); + var indexTemp = indexEvaluation.MakeResultTemp(); tests.Add(MakeTestsAndBindings(indexTemp, subpattern, bindings)); } } if (list.VariableAccess is not null) { + BoundDagTemp input = PrepareForUnionValuePropertyMatching(ref inputInfo, tests); bindings.Add(new BoundPatternBinding(list.VariableAccess, input)); } + outputInfo = inputInfo; return Tests.AndSequence.Create(tests); } } diff --git a/src/Compilers/CSharp/Portable/Binder/ForEachEnumeratorInfo.cs b/src/Compilers/CSharp/Portable/Binder/ForEachEnumeratorInfo.cs index 43480ad6ebd6..d3c028cbabc2 100644 --- a/src/Compilers/CSharp/Portable/Binder/ForEachEnumeratorInfo.cs +++ b/src/Compilers/CSharp/Portable/Binder/ForEachEnumeratorInfo.cs @@ -151,6 +151,23 @@ public ForEachEnumeratorInfo Build(BinderFlags location) public bool IsIncomplete => GetEnumeratorInfo is null || MoveNextInfo is null || CurrentPropertyGetter is null; + + public readonly void ReportDiagnosticsIfUnsafeMemberAccess(Binder binder, SyntaxNodeOrToken node, SyntaxNode syntax, BindingDiagnosticBag diagnostics) + { + var getEnumeratorMethod = this.GetEnumeratorInfo?.Method; + if (getEnumeratorMethod is not null) binder.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, getEnumeratorMethod, node); + var moveNextMethod = this.MoveNextInfo?.Method; + if (moveNextMethod is not null) binder.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, moveNextMethod, node); + var currentPropertyGetter = this.CurrentPropertyGetter; + if (currentPropertyGetter is not null) binder.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, currentPropertyGetter, node); + + // Diagnostics for pattern-based Dispose method are reported elsewhere. + if (this.NeedsDisposal && this.PatternDisposeInfo?.Method is null && + LocalRewriter.TryGetDisposeMethod(binder.Compilation, syntax, this.IsAsync, BindingDiagnosticBag.Discarded, out var disposeMethod)) + { + binder.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, disposeMethod, node); + } + } } } } diff --git a/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs b/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs index 2d6af3dcfaf7..e47f2b1f2e84 100644 --- a/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/ForEachLoopBinder.cs @@ -475,6 +475,8 @@ private BoundForEachStatement BindForEachPartsWorker(BindingDiagnosticBag diagno Debug.Assert(!IsDisallowedExtensionInOlderLangVer(builder.MoveNextInfo.Method)); Debug.Assert(!IsDisallowedExtensionInOlderLangVer(builder.CurrentPropertyGetter)); + builder.ReportDiagnosticsIfUnsafeMemberAccess(this, foreachKeyword, _syntax, diagnostics); + // We want to convert from inferredType in the array/string case and builder.ElementType in the enumerator case, // but it turns out that these are equivalent (when both are available). @@ -644,6 +646,7 @@ protected BoundExpression ConvertForEachCollection( // We're wrapping the collection expression in a (non-synthesized) conversion so that its converted // type (i.e. builder.CollectionType) will be available in the binding API. Debug.Assert(!collectionConversionClassification.IsUserDefined); + Debug.Assert(!collectionConversionClassification.IsUnion); BoundExpression convertedCollectionExpression = CreateConversion( collectionExpr.Syntax, @@ -1556,6 +1559,7 @@ private MethodArgumentInfo FindForEachPatternMethodViaExtension(SyntaxNode synta // Unconditionally convert here, to match what we set the ConvertedExpression to in the main BoundForEachStatement node. Debug.Assert(!collectionConversion.IsUserDefined); + Debug.Assert(!collectionConversion.IsUnion); collectionExpr = new BoundConversion( collectionExpr.Syntax, collectionExpr, diff --git a/src/Compilers/CSharp/Portable/Binder/InContainerBinder.cs b/src/Compilers/CSharp/Portable/Binder/InContainerBinder.cs index c7e0d48f2064..9c0612b12d2e 100644 --- a/src/Compilers/CSharp/Portable/Binder/InContainerBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/InContainerBinder.cs @@ -72,25 +72,12 @@ internal override bool SupportsExtensions get { return true; } } - internal override void GetCandidateExtensionMethodsInSingleBinder( - ArrayBuilder methods, - string name, - int arity, - LookupOptions options, - Binder originalBinder) - { - if (_container.Kind == SymbolKind.Namespace) - { - ((NamespaceSymbol)_container).GetExtensionMethods(methods, name, arity, options); - } - } - #nullable enable - internal override void GetCandidateExtensionMembersInSingleBinder(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, Binder originalBinder) + internal override void GetAllExtensionCandidatesInSingleBinder(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, Binder originalBinder) { if (_container is NamespaceSymbol ns) { - ns.GetExtensionMembers(members, name, alternativeName, arity, options, originalBinder.FieldsBeingBound); + ns.GetAllExtensionMembers(members, name, alternativeName, arity, options, originalBinder.FieldsBeingBound); } } #nullable disable diff --git a/src/Compilers/CSharp/Portable/Binder/InSubmissionClassBinder.cs b/src/Compilers/CSharp/Portable/Binder/InSubmissionClassBinder.cs index 635d452c4638..8fdcd3654803 100644 --- a/src/Compilers/CSharp/Portable/Binder/InSubmissionClassBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/InSubmissionClassBinder.cs @@ -31,16 +31,11 @@ internal InSubmissionClassBinder(NamedTypeSymbol submissionClass, Binder next, C _inUsings = inUsings; } - internal override void GetCandidateExtensionMethodsInSingleBinder( - ArrayBuilder methods, - string name, - int arity, - LookupOptions options, - Binder originalBinder) + internal override void GetAllExtensionCandidatesInSingleBinder(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, Binder originalBinder) { for (var submission = this.Compilation; submission != null; submission = submission.PreviousSubmission) { - submission.ScriptClass?.GetExtensionMethods(methods, name, arity, options); + submission.ScriptClass?.GetAllExtensionMembers(members, name, alternativeName, arity, options, fieldsBeingBound: ConsList.Empty); } } diff --git a/src/Compilers/CSharp/Portable/Binder/LockBinder.cs b/src/Compilers/CSharp/Portable/Binder/LockBinder.cs index 0899da0cfdb6..bd67774a35ec 100644 --- a/src/Compilers/CSharp/Portable/Binder/LockBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/LockBinder.cs @@ -64,6 +64,10 @@ internal override BoundStatement BindLockStatementParts(BindingDiagnosticBag dia _ = diagnostics.ReportUseSite(lockTypeInfo.EnterScopeMethod, exprSyntax) || diagnostics.ReportUseSite(lockTypeInfo.ScopeType, exprSyntax) || diagnostics.ReportUseSite(lockTypeInfo.ScopeDisposeMethod, exprSyntax); + + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, lockTypeInfo.EnterScopeMethod, exprSyntax); + AssertNotUnsafeMemberAccess(lockTypeInfo.ScopeType); + ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, lockTypeInfo.ScopeDisposeMethod, exprSyntax); } BoundStatement stmt = originalBinder.BindPossibleEmbeddedStatement(_syntax.Statement, diagnostics); diff --git a/src/Compilers/CSharp/Portable/Binder/NameofBinder.cs b/src/Compilers/CSharp/Portable/Binder/NameofBinder.cs index acf4ba3dfcf5..19c4be7eeab3 100644 --- a/src/Compilers/CSharp/Portable/Binder/NameofBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/NameofBinder.cs @@ -81,6 +81,7 @@ internal override void LookupSymbolsInSingleBinder(LookupResult result, string n var tmp = LookupResult.GetInstance(); _withTypeParametersBinder.LookupSymbolsInSingleBinder(tmp, name, arity, basesBeingResolved, options, originalBinder, diagnose, ref useSiteInfo); result.MergeEqual(tmp); + tmp.Free(); } else { diff --git a/src/Compilers/CSharp/Portable/Binder/PatternExplainer.cs b/src/Compilers/CSharp/Portable/Binder/PatternExplainer.cs index 37a4858592cc..0cbfee5fa358 100644 --- a/src/Compilers/CSharp/Portable/Binder/PatternExplainer.cs +++ b/src/Compilers/CSharp/Portable/Binder/PatternExplainer.cs @@ -298,7 +298,7 @@ static void gatherConstraintsAndEvaluations(BoundDecisionDagNode targetNode, Imm bool sense = t.WhenTrue == nextNode || (t.WhenFalse != nextNode && t.WhenTrue is BoundWhenDecisionDagNode); BoundDagTest test = t.Test; BoundDagTemp temp = test.Input; - if (test is BoundDagTypeTest && sense == false) + if (test is BoundDagTypeTest && sense == false && ValueSetFactory.TypeUnionValueSetFactoryForInput(test.Input) is null) { // A failed type test is not very useful in constructing a counterexample, // at least not without discriminated unions, so we just drop them. @@ -340,6 +340,7 @@ private static string SamplePatternForTemp( return tryHandleSingleTest() ?? + tryHandleTypeUnionLimits() ?? tryHandleTypeTestAndTypeEvaluation(ref unnamedEnumValue) ?? tryHandleUnboxNullableValueType(ref unnamedEnumValue) ?? tryHandleTuplePattern(ref unnamedEnumValue) ?? @@ -376,13 +377,28 @@ string tryHandleSingleTest() // Handle the special case of a type test and a type evaluation. string tryHandleTypeTestAndTypeEvaluation(ref bool unnamedEnumValue) { - if (evaluations.Length == 1 && constraints.Length == 1 && - constraints[0] is (BoundDagTypeTest { Type: var constraintType }, true) && - evaluations[0] is BoundDagTypeEvaluation { Type: var evaluationType } te && - constraintType.Equals(evaluationType, TypeCompareKind.AllIgnoreOptions)) + if (evaluations is [BoundDagTypeEvaluation { Type: var evaluationType } te]) { - var typedTemp = new BoundDagTemp(te.Syntax, te.Type, te); - return SamplePatternForTemp(typedTemp, constraintMap, evaluationMap, requireExactType: true, ref unnamedEnumValue); + TypeSymbol constraintType = null; + bool sense = false; + + if (constraints is [(BoundDagTypeTest { Type: var constraintType1 }, true)]) + { + constraintType = constraintType1; + sense = true; + } + else if (constraints is [(BoundDagNonNullTest, true) or (BoundDagExplicitNullTest, false), (BoundDagTypeTest { Type: var constraintType2 }, var sense2)]) + { + constraintType = constraintType2; + sense = sense2; + } + + if (constraintType is not null && + constraintType.Equals(evaluationType, TypeCompareKind.AllIgnoreOptions) == sense) + { + var typedTemp = te.MakeResultTemp(); + return SamplePatternForTemp(typedTemp, constraintMap, evaluationMap, requireExactType: true, ref unnamedEnumValue); + } } return null; @@ -396,7 +412,7 @@ constraints[0] is (BoundDagNonNullTest _, true) && evaluations[0] is BoundDagTypeEvaluation { Type: var evaluationType } te && input.Type.IsNullableType() && input.Type.GetNullableUnderlyingType().Equals(evaluationType, TypeCompareKind.AllIgnoreOptions)) { - var typedTemp = new BoundDagTemp(te.Syntax, te.Type, te); + var typedTemp = te.MakeResultTemp(); var result = SamplePatternForTemp(typedTemp, constraintMap, evaluationMap, requireExactType: false, ref unnamedEnumValue); // We need a null check. If not included in the result, add it. return (result == "_") ? "not null" : result; @@ -440,8 +456,8 @@ string tryHandleListPattern(ref bool unnamedEnumValue) } } - var lengthTemp = new BoundDagTemp(lengthOrCount.Syntax, lengthOrCount.Property.Type, lengthOrCount); - var lengthValues = (IValueSet)computeRemainingValues(ValueSetFactory.ForLength, getArray(constraintMap, lengthTemp)); + var lengthTemp = lengthOrCount.MakeResultTemp(); + var lengthValues = (IConstantValueSet)computeRemainingValues(ValueSetFactory.ForLength, getArray(constraintMap, lengthTemp)); int lengthValue = lengthValues.Sample.Int32Value; if (slice != null) { @@ -468,7 +484,7 @@ string tryHandleListPattern(ref bool unnamedEnumValue) switch (evaluations[i]) { case BoundDagIndexerEvaluation e: - var indexerTemp = new BoundDagTemp(e.Syntax, e.IndexerType, e); + var indexerTemp = e.MakeResultTemp(); int index = e.Index; int effectiveIndex = index < 0 ? lengthValue + index : index; if (effectiveIndex < 0 || effectiveIndex >= lengthValue) @@ -487,7 +503,7 @@ string tryHandleListPattern(ref bool unnamedEnumValue) if (slice != null) { - var sliceTemp = new BoundDagTemp(slice.Syntax, slice.SliceType, slice); + var sliceTemp = slice.MakeResultTemp(); var slicePattern = SamplePatternForTemp(sliceTemp, constraintMap, evaluationMap, requireExactType: false, ref unnamedEnumValue); if (slicePattern != "_") { @@ -516,7 +532,7 @@ string tryHandleTuplePattern(ref bool unnamedEnumValue) subpatterns.AddMany("_", cardinality); foreach (BoundDagFieldEvaluation e in evaluations) { - var elementTemp = new BoundDagTemp(e.Syntax, e.Field.Type, e); + var elementTemp = e.MakeResultTemp(); var index = e.Field.TupleElementIndex; if (index < 0 || index >= cardinality) return null; @@ -547,8 +563,8 @@ string tryHandleNumericLimits(ref bool unnamedEnumValue) ValueSetFactory.ForInput(input) is { } fac) { // All we have are numeric constraints. Process them to compute a value not covered. - IValueSet remainingValues = computeRemainingValues(fac, constraints); - if (remainingValues.Complement().IsEmpty) + IConstantValueSet remainingValues = computeRemainingValues(fac, constraints); + if (((IConstantValueSet)remainingValues.Complement()).IsEmpty) return "_"; return SampleValueString(remainingValues, input.Type, requireExactType: requireExactType, unnamedEnumValue: ref unnamedEnumValue); @@ -557,6 +573,64 @@ string tryHandleNumericLimits(ref bool unnamedEnumValue) return null; } + string tryHandleTypeUnionLimits() + { + if (evaluations.IsEmpty && ValueSetFactory.TypeUnionValueSetFactoryForInput(input) is { } factory && + constraints.All(t => t switch + { + (BoundDagTypeTest _, _) => true, + (BoundDagExplicitNullTest _, sense: false) => true, + (BoundDagNonNullTest _, sense: true) => true, + _ => false + })) + { + var conversions = input.Type.ContainingAssembly.TypeConversions; + var remainingValues = factory.AllValues(conversions); + var discardedInfo = CompoundUseSiteInfo.Discarded; + foreach (var constraint in constraints) + { + var (test, sense) = constraint; + + TypeUnionValueSet filtered; + + switch (test) + { + case BoundDagTypeTest typeTest: + filtered = factory.FromTypeMatch(typeTest.Type, conversions, ref discardedInfo); + break; + case BoundDagExplicitNullTest: + filtered = factory.FromNullMatch(conversions); + break; + case BoundDagNonNullTest: + filtered = factory.FromNonNullMatch(conversions); + break; + default: + throw ExceptionUtilities.UnexpectedValue(test); + } + + if (!sense) + { + filtered = filtered.Complement(); + } + + remainingValues = remainingValues.Intersect(filtered); + } + + if (remainingValues.IsEmpty(ref discardedInfo)) + return null; + + if (remainingValues.SampleType(ref discardedInfo) is { } type) + { + return type.ToDisplayString(); + } + + if (remainingValues.IncludesNull) + return "null"; + } + + return null; + } + // Handle the special case of a recursive pattern string tryHandleRecursivePattern(ref bool unnamedEnumValue) { @@ -582,14 +656,23 @@ string tryHandleRecursivePattern(ref bool unnamedEnumValue) int extensionExtra = method.RequiresInstanceReceiver ? 0 : 1; int count = method.Parameters.Length - extensionExtra; var subpatternBuilder = new StringBuilder("("); - for (int j = 0; j < count; j++) + ArrayBuilder outParamTemps = e.MakeOutParameterTemps(); + bool first = true; + foreach (var elementTemp in outParamTemps) { - var elementTemp = new BoundDagTemp(e.Syntax, method.Parameters[j + extensionExtra].Type, e, j); var newPattern = SamplePatternForTemp(elementTemp, constraintMap, evaluationMap, requireExactType: false, ref unnamedEnumValue); - if (j != 0) + if (first) + { + first = false; + } + else + { subpatternBuilder.Append(", "); + } + subpatternBuilder.Append(newPattern); } + outParamTemps.Free(); subpatternBuilder.Append(')'); var result = subpatternBuilder.ToString(); if (deconstruction != null && needsPropertyString) @@ -603,15 +686,23 @@ string tryHandleRecursivePattern(ref bool unnamedEnumValue) break; case BoundDagFieldEvaluation e: { - var subInput = new BoundDagTemp(e.Syntax, e.Field.Type, e); + var subInput = e.MakeResultTemp(); var subPattern = SamplePatternForTemp(subInput, constraintMap, evaluationMap, false, ref unnamedEnumValue); properties.Add(e.Field, subPattern); } break; case BoundDagPropertyEvaluation e: { - var subInput = new BoundDagTemp(e.Syntax, e.Property.Type, e); + var subInput = e.MakeResultTemp(); var subPattern = SamplePatternForTemp(subInput, constraintMap, evaluationMap, false, ref unnamedEnumValue); + + if (evaluations.Length == 1 && e.Property is { Name: WellKnownMemberNames.ValuePropertyName } property && + e.Input.Type is NamedTypeSymbol { IsUnionType: true } unionType && + Binder.IsUnionTypeValueProperty(unionType, property)) + { + return subPattern; + } + properties.Add(e.Property, subPattern); } break; @@ -633,9 +724,9 @@ string produceFallbackPattern() return requireExactType ? input.Type.ToDisplayString() : "_"; } - IValueSet computeRemainingValues(IValueSetFactory fac, ImmutableArray<(BoundDagTest test, bool sense)> constraints) + IConstantValueSet computeRemainingValues(IConstantValueSetFactory fac, ImmutableArray<(BoundDagTest test, bool sense)> constraints) { - var remainingValues = fac.AllValues; + IConstantValueSet remainingValues = fac.AllValues; foreach (var constraint in constraints) { var (test, sense) = constraint; @@ -653,10 +744,10 @@ void addRelation(BinaryOperatorKind relation, ConstantValue value) { if (value.IsBad) return; - var filtered = fac.Related(relation, value); + IConstantValueSet filtered = fac.Related(relation, value); if (!sense) - filtered = filtered.Complement(); - remainingValues = remainingValues.Intersect(filtered); + filtered = (IConstantValueSet)filtered.Complement(); + remainingValues = (IConstantValueSet)remainingValues.Intersect(filtered); } } @@ -678,7 +769,7 @@ static bool isNotNullTest((BoundDagTest test, bool sense) constraint) } } - private static string SampleValueString(IValueSet remainingValues, TypeSymbol type, bool requireExactType, ref bool unnamedEnumValue) + private static string SampleValueString(IConstantValueSet remainingValues, TypeSymbol type, bool requireExactType, ref bool unnamedEnumValue) { // In rare cases it's possible the DAG path we analyzed yields empty remaining values if (remainingValues.IsEmpty) diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/Conversion.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/Conversion.cs index fdb7132fba85..64c3abeab492 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/Conversion.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/Conversion.cs @@ -9,6 +9,7 @@ using System.Collections.Immutable; using System.Diagnostics; using Microsoft.CodeAnalysis.Operations; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.CodeAnalysis.CSharp { @@ -136,6 +137,17 @@ internal static Conversion CreateCollectionExpressionConversion( new CollectionExpressionUncommonData(collectionExpressionTypeKind, elementType, constructor, constructorUsedInExpandedForm, elementConversions)); } + internal static Conversion CreateUnionConversion(UserDefinedConversionResult conversionResult) + { + return new Conversion( + ConversionKind.Union, + new MethodUncommonData( + isExtensionMethod: false, + isArrayIndex: false, + conversionResult: conversionResult, + conversionMethod: null)); + } + private Conversion( ConversionKind kind, UncommonData? uncommonData = null) @@ -433,6 +445,13 @@ internal void AssertUnderlyingConversionsCheckedRecursive() UserDefinedFromConversion.AssertUnderlyingConversionsCheckedRecursive(); UserDefinedToConversion.AssertUnderlyingConversionsCheckedRecursive(); } + else if (IsUnion) + { + Debug.Assert(BestUnionConversionAnalysis is { }); + var analysis = BestUnionConversionAnalysis; + analysis.SourceConversion.AssertUnderlyingConversionsCheckedRecursive(); + analysis.TargetConversion.AssertUnderlyingConversionsCheckedRecursive(); + } } [Conditional("DEBUG")] @@ -472,6 +491,13 @@ internal void MarkUnderlyingConversionsCheckedRecursive() UserDefinedFromConversion.MarkUnderlyingConversionsCheckedRecursive(); UserDefinedToConversion.MarkUnderlyingConversionsCheckedRecursive(); } + else if (IsUnion) + { + Debug.Assert(BestUnionConversionAnalysis is { }); + var analysis = BestUnionConversionAnalysis; + analysis.SourceConversion.MarkUnderlyingConversionsCheckedRecursive(); + analysis.TargetConversion.MarkUnderlyingConversionsCheckedRecursive(); + } } #endif } @@ -581,10 +607,11 @@ internal bool IsValid } Debug.Assert(!this.IsUserDefined); + Debug.Assert(!this.IsUnion); return true; } - return !this.IsUserDefined || + return (!this.IsUserDefined && !this.IsUnion) || this.Method is object || (_uncommonData as MethodUncommonData)?._conversionResult.Kind == UserDefinedConversionResultKind.Valid; } @@ -851,6 +878,19 @@ public bool IsUserDefined } } + /// + /// Returns true if the conversion is an implicit union conversion. + /// + [MemberNotNullWhen(true, nameof(BestUnionConversionAnalysis))] + public bool IsUnion + { + [Experimental(RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = "https://github.com/dotnet/roslyn/issues/82567")] + get + { + return Kind.IsUnionConversion(); + } + } + /// /// Returns true if the conversion is an implicit boxing conversion. /// @@ -1009,6 +1049,7 @@ public bool IsIntPtr /// /// Returns the method used to create the delegate for a method group conversion if is true /// or the method used to perform the conversion for a user-defined conversion if is true. + /// or the method used to perform the conversion for a union conversion if is true. /// Otherwise, returns null. /// /// @@ -1111,7 +1152,7 @@ internal ImmutableArray OriginalUserDefinedConversions // the IDE wants information about the *inferred* method, not the original unconstructed // generic method. - if (_uncommonData is MethodUncommonData { _conversionResult: { Kind: not UserDefinedConversionResultKind.NoApplicableOperators } conversionResult }) + if (!IsUnion && _uncommonData is MethodUncommonData { _conversionResult: { Kind: not UserDefinedConversionResultKind.NoApplicableOperators } conversionResult }) { var builder = ArrayBuilder.GetInstance(); foreach (var analysis in conversionResult.Results) @@ -1129,7 +1170,21 @@ internal UserDefinedConversionAnalysis? BestUserDefinedConversionAnalysis { get { - if (_uncommonData is MethodUncommonData { _conversionResult: { Kind: UserDefinedConversionResultKind.Valid } conversionResult }) + if (!IsUnion && _uncommonData is MethodUncommonData { _conversionResult: { Kind: UserDefinedConversionResultKind.Valid } conversionResult }) + { + UserDefinedConversionAnalysis analysis = conversionResult.Results[conversionResult.Best]; + return analysis; + } + + return null; + } + } + + internal UserDefinedConversionAnalysis? BestUnionConversionAnalysis + { + get + { + if (IsUnion && _uncommonData is MethodUncommonData { _conversionResult: { Kind: UserDefinedConversionResultKind.Valid } conversionResult }) { UserDefinedConversionAnalysis analysis = conversionResult.Results[conversionResult.Best]; return analysis; @@ -1150,7 +1205,7 @@ internal UserDefinedConversionAnalysis? BestUserDefinedConversionAnalysis public CommonConversion ToCommonConversion() { // The MethodSymbol of CommonConversion only refers to UserDefined conversions, not method groups - var (methodSymbol, constrainedToType) = IsUserDefined ? (MethodSymbol, ConstrainedToType) : (null, null); + var (methodSymbol, constrainedToType) = IsUserDefined || IsUnion ? (MethodSymbol, ConstrainedToType) : (null, null); return new CommonConversion(Exists, IsIdentity, IsNumeric, IsReference, IsImplicit, IsNullable, methodSymbol, constrainedToType); } diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKind.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKind.cs index e60ed4d88b65..654b746fc1c0 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKind.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKind.cs @@ -71,5 +71,7 @@ internal enum ConversionKind : byte ImplicitSpan, // A conversion from array to (ReadOnly)Span, or from string or (ReadOnly)Span to ReadOnlySpan ExplicitSpan, // A conversion from array to (ReadOnly)Span + + Union, // An implicit conversion to a union type from its case type. } } diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKindExtensions.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKindExtensions.cs index b25b4bef8aaa..cb51c12c42d9 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKindExtensions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionKindExtensions.cs @@ -37,6 +37,7 @@ public static bool IsImplicitConversion(this ConversionKind conversionKind) case ImplicitDynamic: case ImplicitConstant: case ImplicitUserDefined: + case Union: case AnonymousFunction: case ConversionKind.MethodGroup: case ConversionKind.FunctionType: @@ -91,6 +92,11 @@ public static bool IsUserDefinedConversion(this ConversionKind conversionKind) } } + public static bool IsUnionConversion(this ConversionKind conversionKind) + { + return conversionKind == Union; + } + public static bool IsPointerConversion(this ConversionKind kind) { switch (kind) diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/Conversions.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/Conversions.cs index bd685cafcacf..ea25c537a476 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/Conversions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/Conversions.cs @@ -243,6 +243,18 @@ internal Conversion GetCollectionExpressionSpreadElementConversion( targetType, ref useSiteInfo); } + + protected override Conversion AnalyzeImplicitUnionConversions(BoundExpression sourceExpression, TypeSymbol source, TypeSymbol target, ref CompoundUseSiteInfo useSiteInfo) + { + if (_binder.InParameterDefaultValue || _binder.InAttributeArgument) + { + // We don't consider when we're in default parameter values or attributes to avoid cycles. This is an error scenario, + // so we don't care if we accidentally miss a parameter being applicable. + return Conversion.NoConversion; + } + + return base.AnalyzeImplicitUnionConversions(sourceExpression, source, target, ref useSiteInfo); + } #nullable disable /// diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs index 1db49356a231..37bbe15ec24f 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs @@ -143,7 +143,7 @@ public Conversion ClassifyImplicitConversionFromExpression(BoundExpression sourc } } - conversion = GetImplicitUserDefinedConversion(sourceExpression, sourceType, destination, ref useSiteInfo); + conversion = GetImplicitUserDefinedOrUnionConversion(sourceExpression, sourceType, destination, ref useSiteInfo); if (conversion.Exists) { return conversion; @@ -192,7 +192,7 @@ public Conversion ClassifyImplicitConversionFromType(TypeSymbol source, TypeSymb } } - return GetImplicitUserDefinedConversion(source, destination, ref useSiteInfo); + return GetImplicitUserDefinedOrUnionConversion(source, destination, ref useSiteInfo); } /// @@ -344,7 +344,7 @@ public Conversion ClassifyConversionFromType(TypeSymbol source, TypeSymbol desti } } - Conversion conversion = GetImplicitUserDefinedConversion(source, destination, ref useSiteInfo); + Conversion conversion = GetImplicitUserDefinedOrUnionConversion(source, destination, ref useSiteInfo); if (conversion.Exists) { return conversion; @@ -477,7 +477,7 @@ private Conversion ClassifyConversionFromTypeForCast(TypeSymbol source, TypeSymb return conversion; } - return GetImplicitUserDefinedConversion(source, destination, ref useSiteInfo); + return GetImplicitUserDefinedOrUnionConversion(source, destination, ref useSiteInfo); } /// @@ -779,15 +779,29 @@ private Conversion ClassifyImplicitBuiltInConversionSlow(TypeSymbol source, Type return Conversion.NoConversion; } - private Conversion GetImplicitUserDefinedConversion(BoundExpression sourceExpression, TypeSymbol source, TypeSymbol destination, ref CompoundUseSiteInfo useSiteInfo) + private Conversion GetImplicitUserDefinedOrUnionConversion(BoundExpression sourceExpression, TypeSymbol source, TypeSymbol destination, ref CompoundUseSiteInfo useSiteInfo) { var conversionResult = AnalyzeImplicitUserDefinedConversions(sourceExpression, source, destination, ref useSiteInfo); - return new Conversion(conversionResult, isImplicit: true); + var result = new Conversion(conversionResult, isImplicit: true); + + if (result.Exists) + { + return result; + } + + Conversion unionConversion = AnalyzeImplicitUnionConversions(sourceExpression, source, destination, ref useSiteInfo); + + if (unionConversion.Exists) + { + return unionConversion; + } + + return result; } - private Conversion GetImplicitUserDefinedConversion(TypeSymbol source, TypeSymbol destination, ref CompoundUseSiteInfo useSiteInfo) + private Conversion GetImplicitUserDefinedOrUnionConversion(TypeSymbol source, TypeSymbol destination, ref CompoundUseSiteInfo useSiteInfo) { - return GetImplicitUserDefinedConversion(sourceExpression: null, source, destination, ref useSiteInfo); + return GetImplicitUserDefinedOrUnionConversion(sourceExpression: null, source, destination, ref useSiteInfo); } private Conversion ClassifyExplicitBuiltInOnlyConversion(TypeSymbol source, TypeSymbol destination, bool isChecked, ref CompoundUseSiteInfo useSiteInfo, bool forCast) @@ -1012,6 +1026,7 @@ private static bool ExplicitConversionMayDifferFromImplicit(Conversion implicitC switch (implicitConversion.Kind) { case ConversionKind.ImplicitUserDefined: + case ConversionKind.Union: case ConversionKind.ImplicitDynamic: case ConversionKind.ImplicitTuple: case ConversionKind.ImplicitTupleLiteral: @@ -1890,8 +1905,9 @@ public bool HasAnyNullabilityImplicitConversion(TypeWithAnnotations source, Type { Debug.Assert(IncludeNullability); var discardedUseSiteInfo = CompoundUseSiteInfo.Discarded; - return HasTopLevelNullabilityImplicitConversion(source, destination) && - ClassifyImplicitConversionFromType(source.Type, destination.Type, ref discardedUseSiteInfo).Kind != ConversionKind.NoConversion; + Conversion conversion = ClassifyImplicitConversionFromType(source.Type, destination.Type, ref discardedUseSiteInfo); + return conversion.Kind != ConversionKind.NoConversion && + (conversion.IsUnion || conversion.IsUserDefined || HasTopLevelNullabilityImplicitConversion(source, destination)); } private static bool HasIdentityConversionToAny(NamedTypeSymbol type, ArrayBuilder<(NamedTypeSymbol ParticipatingType, TypeParameterSymbol ConstrainedToTypeOpt)> targetTypes) @@ -2364,11 +2380,14 @@ private Conversion ClassifyImplicitTupleConversion(TypeSymbol source, TypeSymbol ConversionKind.ImplicitTuple, (ConversionsBase conversions, TypeWithAnnotations s, TypeWithAnnotations d, bool _, ref CompoundUseSiteInfo u, bool _) => { - if (!conversions.HasTopLevelNullabilityImplicitConversion(s, d)) + Conversion conversion = conversions.ClassifyImplicitConversionFromType(s.Type, d.Type, ref u); + + if (!conversion.IsUserDefined && !conversion.IsUnion && !conversions.HasTopLevelNullabilityImplicitConversion(s, d)) { return Conversion.NoConversion; } - return conversions.ClassifyImplicitConversionFromType(s.Type, d.Type, ref u); + + return conversion; }, isChecked: false, forCast: false); @@ -2383,11 +2402,14 @@ private Conversion ClassifyExplicitTupleConversion(TypeSymbol source, TypeSymbol ConversionKind.ExplicitTuple, (ConversionsBase conversions, TypeWithAnnotations s, TypeWithAnnotations d, bool isChecked, ref CompoundUseSiteInfo u, bool forCast) => { - if (!conversions.HasTopLevelNullabilityImplicitConversion(s, d)) + Conversion conversion = conversions.ClassifyConversionFromType(s.Type, d.Type, isChecked: isChecked, ref u, forCast); + + if (!conversion.IsUserDefined && !conversion.IsUnion && !conversions.HasTopLevelNullabilityImplicitConversion(s, d)) { return Conversion.NoConversion; } - return conversions.ClassifyConversionFromType(s.Type, d.Type, isChecked: isChecked, ref u, forCast); + + return conversion; }, isChecked: isChecked, forCast); diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedImplicitConversions.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedImplicitConversions.cs index 92e44f962385..7d2f4e63c105 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedImplicitConversions.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedImplicitConversions.cs @@ -634,7 +634,9 @@ internal static bool IsEncompassingImplicitConversionKind(ConversionKind kind) // Not "standard". case ConversionKind.ImplicitUserDefined: case ConversionKind.ExplicitUserDefined: + case ConversionKind.Union: case ConversionKind.FunctionType: + case ConversionKind.CollectionExpression: // Not implicit. case ConversionKind.ExplicitNumeric: @@ -978,5 +980,72 @@ protected UserDefinedConversionResult AnalyzeImplicitUserDefinedConversionForV6S return UserDefinedConversionResult.NoApplicableOperators(u); } + + protected virtual Conversion AnalyzeImplicitUnionConversions( + BoundExpression sourceExpression, + TypeSymbol source, + TypeSymbol target, + ref CompoundUseSiteInfo useSiteInfo) + { + Debug.Assert(sourceExpression is null || Compilation is not null); + Debug.Assert(sourceExpression != null || (object)source != null); + Debug.Assert((object)target != null); + + if (target.StrippedType() is not NamedTypeSymbol namedTarget || !namedTarget.IsUnionType) + { + return Conversion.NoConversion; + } + + // SPEC: Find the set of applicable constructors + var ubuild = ArrayBuilder.GetInstance(); + computeApplicableConstructorSet(sourceExpression, source, target, namedTarget, ubuild, ref useSiteInfo); + + if (ubuild.Count == 0) + { + ubuild.Free(); + return Conversion.NoConversion; + } + + ImmutableArray u = ubuild.ToImmutableAndFree(); + + // Find the most specific source type SX of the operators in U... + TypeSymbol sx = MostSpecificSourceTypeForImplicitUserDefinedConversion(u, source, ref useSiteInfo); + if ((object)sx == null || MostSpecificConversionOperator(sx, namedTarget, u) is not int best) + { + // Ambiguous. The first applicable is good enough then. + best = 0; + } + + return Conversion.CreateUnionConversion(UserDefinedConversionResult.Valid(u, best)); + + void computeApplicableConstructorSet( + BoundExpression sourceExpression, + TypeSymbol source, + TypeSymbol target, + NamedTypeSymbol declaringType, + ArrayBuilder u, + ref CompoundUseSiteInfo useSiteInfo) + { + foreach (MethodSymbol ctor in declaringType.InstanceConstructors) + { + if (!NamedTypeSymbol.IsSuitableUnionConstructor(ctor)) + { + continue; + } + + TypeSymbol convertsFrom = ctor.GetParameterType(0); + Conversion fromConversion = EncompassingImplicitConversion(sourceExpression, source, convertsFrom, ref useSiteInfo); + Conversion targetConversion = EncompassingImplicitConversion(declaringType, target, ref useSiteInfo); + + Debug.Assert(targetConversion.Exists && targetConversion.IsImplicit); + Debug.Assert(targetConversion.IsIdentity || (targetConversion.IsNullable && targetConversion.UnderlyingConversions[0].IsIdentity)); + + if (fromConversion.Exists && targetConversion.Exists) + { + u.Add(UserDefinedConversionAnalysis.Normal(constrainedToTypeOpt: null, ctor, fromConversion, targetConversion, convertsFrom, toType: declaringType)); + } + } + } + } } } diff --git a/src/Compilers/CSharp/Portable/Binder/Semantics/OverloadResolution/MethodTypeInference.cs b/src/Compilers/CSharp/Portable/Binder/Semantics/OverloadResolution/MethodTypeInference.cs index 0ee516536924..9abe0240201f 100644 --- a/src/Compilers/CSharp/Portable/Binder/Semantics/OverloadResolution/MethodTypeInference.cs +++ b/src/Compilers/CSharp/Portable/Binder/Semantics/OverloadResolution/MethodTypeInference.cs @@ -3048,12 +3048,8 @@ private static bool ImplicitConversionExists(TypeWithAnnotations sourceWithAnnot return false; } - if (!conversions.HasTopLevelNullabilityImplicitConversion(sourceWithAnnotations, destinationWithAnnotations)) - { - return false; - } - - return conversions.ClassifyImplicitConversionFromTypeWhenNeitherOrBothFunctionTypes(source, destination, ref useSiteInfo).Exists; + Conversion conversion = conversions.ClassifyImplicitConversionFromTypeWhenNeitherOrBothFunctionTypes(source, destination, ref useSiteInfo); + return conversion.Exists && (conversion.IsUnion || conversion.IsUserDefined || conversions.HasTopLevelNullabilityImplicitConversion(sourceWithAnnotations, destinationWithAnnotations)); } #nullable disable diff --git a/src/Compilers/CSharp/Portable/Binder/SwitchBinder.cs b/src/Compilers/CSharp/Portable/Binder/SwitchBinder.cs index ec238a709c8b..e8b6896c0721 100644 --- a/src/Compilers/CSharp/Portable/Binder/SwitchBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/SwitchBinder.cs @@ -223,8 +223,9 @@ private void BuildSwitchLabels(SyntaxList labelsSyntax, Binde case SyntaxKind.CasePatternSwitchLabel: // bind the pattern, to cause its pattern variables to be inferred if necessary var matchLabel = (CasePatternSwitchLabelSyntax)labelSyntax; + NamedTypeSymbol unionType = null; _ = sectionBinder.BindPattern( - matchLabel.Pattern, SwitchGoverningType, permitDesignations: true, labelSyntax.HasErrors, tempDiagnosticBag); + matchLabel.Pattern, ref unionType, SwitchGoverningType, permitDesignations: true, labelSyntax.HasErrors, tempDiagnosticBag, hasUnionMatching: out _); break; default: @@ -267,10 +268,16 @@ protected BoundExpression ConvertCaseExpression(CSharpSyntaxNode node, BoundExpr hasErrors = true; } - caseExpression = CreateConversion(caseExpression, conversion, SwitchGoverningType, diagnostics); + if (!conversion.IsUnion && !(caseExpression.IsLiteralNull() && SwitchGoverningType.StrippedType() is NamedTypeSymbol { IsUnionType: true })) + { + caseExpression = CreateConversion(caseExpression, conversion, SwitchGoverningType, diagnostics); + } } - return ConvertPatternExpression(SwitchGoverningType, node, caseExpression, out constantValueOpt, hasErrors, diagnostics, out _); + var inputType = SwitchGoverningType; + NamedTypeSymbol unionType = null; + PrepareForUnionMatchingIfAppropriateAndReturnUnionMatchingInputType(node, ref inputType, ref unionType, diagnostics); + return ConvertPatternExpression(unionType, inputType, node, caseExpression, out constantValueOpt, hasErrors, diagnostics, out _); } private static readonly object s_nullKey = new object(); diff --git a/src/Compilers/CSharp/Portable/Binder/SwitchBinder_Patterns.cs b/src/Compilers/CSharp/Portable/Binder/SwitchBinder_Patterns.cs index c6553470250b..6d4ead41e237 100644 --- a/src/Compilers/CSharp/Portable/Binder/SwitchBinder_Patterns.cs +++ b/src/Compilers/CSharp/Portable/Binder/SwitchBinder_Patterns.cs @@ -131,7 +131,7 @@ static bool isSubsumed(BoundSwitchLabel switchLabel, ImmutableHashSet locals = _armScopeBinder.Locals; - BoundPattern pattern = armBinder.BindPattern(node.Pattern, switchGoverningType, permitDesignations: true, hasErrors, diagnostics); + NamedTypeSymbol? unionType = null; + BoundPattern pattern = armBinder.BindPattern(node.Pattern, ref unionType, switchGoverningType, permitDesignations: true, hasErrors, diagnostics, out bool hasUnionMatching); BoundExpression? whenClause = node.WhenClause != null ? armBinder.BindBooleanExpression(node.WhenClause.Condition, diagnostics) : null; BoundExpression armResult = armBinder.BindValue(node.Expression, diagnostics, BindValueKind.RValue); var label = new GeneratedLabelSymbol("arm"); - return new BoundSwitchExpressionArm(node, locals, pattern, whenClause, armResult, label, hasErrors | pattern.HasErrors); + return new BoundSwitchExpressionArm(node, locals, pattern, hasUnionMatching, whenClause, armResult, label, hasErrors | pattern.HasErrors); } } } diff --git a/src/Compilers/CSharp/Portable/Binder/UnionMatchingRewriter.cs b/src/Compilers/CSharp/Portable/Binder/UnionMatchingRewriter.cs new file mode 100644 index 000000000000..e7bc44a5cf54 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Binder/UnionMatchingRewriter.cs @@ -0,0 +1,503 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.CSharp +{ + /// + /// Rewrite special Union type matching with appropriate BoundRecursivePatterns + /// against an IUnion.Value property. + /// + /// The rewrite happens bottom-up. Nodes that require special Union treatment are represented with + /// a node created during the rewrite. Rewriter keeps a + /// node at the top of the result until we reach a point + /// when we are ready to perform its transformation, we call + /// helper at that point. + /// Generally, the transformation must be performed for a pattern when, and only when, we know that no + /// more conjunctions coming where the pattern could be the left hand side. + /// + /// Assuming that '^' marks a Union matching pattern: + /// + /// A pattern 'unionTypeInstance is int^' is transformed to 'unionTypeInstance is { Value: int }'. + /// + /// A pattern 'unionTypeInstance is not^(int or string)' is transformed to 'unionTypeInstance is { Value: not (int or string) }'. + /// + /// A pattern 'unionTypeInstance is int^ or string^' is transformed to 'unionTypeInstance is { Value: int } or { Value: string }'. + /// + /// A pattern 'unionTypeInstance is int^ and 15 or string^' is transformed to 'unionTypeInstance is { Value: int and 15 } or { Value: string }'. + /// + sealed class UnionMatchingRewriter : BoundTreeRewriter + { + private readonly CSharpCompilation _compilation; + + private UnionMatchingRewriter(CSharpCompilation compilation) + { + _compilation = compilation; + } + + public static BoundPattern Rewrite(CSharpCompilation compilation, BoundPattern pattern) + { + var result = new UnionMatchingRewriter(compilation).Visit(pattern); + Debug.Assert(result != pattern); + return RewritePatternWithUnionMatchingToPropertyPattern((BoundPattern)result); + } + + protected override BoundNode? VisitExpressionOrPatternWithoutStackGuard(BoundNode node) + { + return Visit(node); + } + + private NamedTypeSymbol ObjectType => _compilation.GetSpecialType(SpecialType.System_Object); + + private static BoundPatternWithUnionMatching CreatePatternWithUnionMatching(NamedTypeSymbol unionMatchingInputType, BoundPattern innerPattern) + { + Debug.Assert(unionMatchingInputType.IsSubjectForUnionMatching); + Debug.Assert(innerPattern.InputType.IsObjectType()); + + PropertySymbol? valueProperty = Binder.GetUnionTypeValuePropertyNoUseSiteDiagnostics((NamedTypeSymbol)unionMatchingInputType.StrippedType()); + + var member = new BoundPropertySubpatternMember(innerPattern.Syntax, receiver: null, valueProperty, type: innerPattern.InputType, hasErrors: valueProperty is null).MakeCompilerGenerated(); + + return new BoundPatternWithUnionMatching( + syntax: innerPattern.Syntax, + unionMatchingInputType, + member, + innerPattern, + inputType: unionMatchingInputType).MakeCompilerGenerated(); + } + + public override BoundNode? VisitConstantPattern(BoundConstantPattern node) + { + node = (BoundConstantPattern)base.VisitConstantPattern(node)!; + if (node.IsUnionMatching) + { + Debug.Assert(node.InputType.IsSubjectForUnionMatching); + + if (Binder.IsClassOrNullableValueTypeUnionNullPatternMatching((NamedTypeSymbol)node.InputType, node.ConstantValue) && node.NarrowedType.Equals(node.InputType, TypeCompareKind.AllIgnoreOptions)) + { + // Special case of a null test for a class Union. Its meaning is equivalent to: ( is null or .Value is null) + // Or a special case of a null test for a Nullable. Its meaning is equivalent to: ( is null or .GetValueOrDefault().Value is null) + BoundPatternWithUnionMatching underlyingValueMatching = CreatePatternWithUnionMatching( + (NamedTypeSymbol)node.InputType, + node.Update(node.Value, node.ConstantValue, isUnionMatching: false, inputType: ObjectType, narrowedType: ObjectType)); + + return new BoundBinaryPattern( + node.Syntax, disjunction: true, + left: node.Update(node.Value, node.ConstantValue, isUnionMatching: false, node.InputType, node.InputType), + right: RewritePatternWithUnionMatchingToPropertyPattern(underlyingValueMatching), + inputType: node.InputType, + narrowedType: node.InputType) + { WasCompilerGenerated = true }; + } + + return CreatePatternWithUnionMatching( + (NamedTypeSymbol)node.InputType, + node.Update(node.Value, node.ConstantValue, isUnionMatching: false, inputType: ObjectType, narrowedType: node.NarrowedType)); + } + + return node; + } + + public override BoundNode? VisitRecursivePattern(BoundRecursivePattern node) + { + node = (BoundRecursivePattern)base.VisitRecursivePattern(node)!; + if (node.IsUnionMatching) + { + return CreatePatternWithUnionMatching( + (NamedTypeSymbol)node.InputType, + node.Update( + node.DeclaredType, node.DeconstructMethod, node.Deconstruction, node.Properties, node.IsExplicitNotNullTest, node.Variable, node.VariableAccess, + isUnionMatching: false, inputType: ObjectType, narrowedType: node.NarrowedType)); + } + + return node; + } + + public override BoundNode? VisitListPattern(BoundListPattern node) + { + Symbol? variable = node.Variable; + ImmutableArray subpatterns = this.VisitList(node.Subpatterns).SelectAsArray(RewritePatternWithUnionMatchingToPropertyPattern); + BoundExpression? lengthAccess = node.LengthAccess; + BoundExpression? indexerAccess = node.IndexerAccess; + BoundListPatternReceiverPlaceholder? receiverPlaceholder = node.ReceiverPlaceholder; + BoundListPatternIndexPlaceholder? argumentPlaceholder = node.ArgumentPlaceholder; + BoundExpression? variableAccess = node.VariableAccess; + TypeSymbol? inputType = node.InputType; + TypeSymbol? narrowedType = node.NarrowedType; + + if (node.IsUnionMatching) + { + return CreatePatternWithUnionMatching( + (NamedTypeSymbol)node.InputType, + node.Update(subpatterns, node.HasSlice, lengthAccess, indexerAccess, receiverPlaceholder, argumentPlaceholder, variable, variableAccess, + isUnionMatching: false, inputType: ObjectType, narrowedType)); + } + + return node.Update(subpatterns, node.HasSlice, lengthAccess, indexerAccess, receiverPlaceholder, argumentPlaceholder, variable, variableAccess, isUnionMatching: false, inputType, narrowedType); + } + + public override BoundNode? VisitITuplePattern(BoundITuplePattern node) + { + node = (BoundITuplePattern)base.VisitITuplePattern(node)!; + if (node.IsUnionMatching) + { + return CreatePatternWithUnionMatching( + (NamedTypeSymbol)node.InputType, + node.Update(node.GetLengthMethod, node.GetItemMethod, node.Subpatterns, + isUnionMatching: false, inputType: ObjectType, narrowedType: node.NarrowedType)); + } + + return node; + } + + public override BoundNode? VisitDeclarationPattern(BoundDeclarationPattern node) + { + node = (BoundDeclarationPattern)base.VisitDeclarationPattern(node)!; + if (node.IsUnionMatching) + { + return CreatePatternWithUnionMatching( + (NamedTypeSymbol)node.InputType, + node.Update(node.DeclaredType, node.IsVar, node.Variable, node.VariableAccess, + isUnionMatching: false, inputType: ObjectType, narrowedType: node.NarrowedType)); + } + + return node; + } + + public override BoundNode? VisitTypePattern(BoundTypePattern node) + { + node = (BoundTypePattern)base.VisitTypePattern(node)!; + if (node.IsUnionMatching) + { + return CreatePatternWithUnionMatching( + (NamedTypeSymbol)node.InputType, + node.Update(node.DeclaredType, node.IsExplicitNotNullTest, isUnionMatching: false, inputType: ObjectType, narrowedType: node.NarrowedType)); + } + + return node; + } + + public override BoundNode? VisitRelationalPattern(BoundRelationalPattern node) + { + node = (BoundRelationalPattern)base.VisitRelationalPattern(node)!; + if (node.IsUnionMatching) + { + return CreatePatternWithUnionMatching( + (NamedTypeSymbol)node.InputType, + node.Update(node.Relation, node.Value, node.ConstantValue, isUnionMatching: false, inputType: ObjectType, narrowedType: node.NarrowedType)); + } + + return node; + } + + public override BoundNode? VisitNegatedPattern(BoundNegatedPattern node) + { + BoundPattern negated = RewritePatternWithUnionMatchingToPropertyPattern((BoundPattern)this.Visit(node.Negated)); + TypeSymbol? inputType = node.InputType; + TypeSymbol? narrowedType = node.NarrowedType; + + if (node.IsUnionMatching) + { + return CreatePatternWithUnionMatching( + (NamedTypeSymbol)node.InputType, + node.Update(negated, isUnionMatching: false, inputType: ObjectType, narrowedType)); + } + + return node.Update(negated, isUnionMatching: false, inputType, narrowedType); + } + + public override BoundNode? VisitSlicePattern(BoundSlicePattern node) + { + BoundPattern? pattern = RewritePatternWithUnionMatchingToPropertyPattern((BoundPattern)this.Visit(node.Pattern)); + BoundExpression? indexerAccess = node.IndexerAccess; + BoundSlicePatternReceiverPlaceholder? receiverPlaceholder = node.ReceiverPlaceholder; + BoundSlicePatternRangePlaceholder? argumentPlaceholder = node.ArgumentPlaceholder; + TypeSymbol? inputType = node.InputType; + TypeSymbol? narrowedType = node.NarrowedType; + return node.Update(pattern, indexerAccess, receiverPlaceholder, argumentPlaceholder, inputType, narrowedType); + } + + public override BoundNode? VisitPositionalSubpattern(BoundPositionalSubpattern node) + { + Symbol? symbol = node.Symbol; + BoundPattern pattern = RewritePatternWithUnionMatchingToPropertyPattern((BoundPattern)this.Visit(node.Pattern)); + return node.Update(symbol, pattern); + } + + public override BoundNode? VisitPropertySubpattern(BoundPropertySubpattern node) + { + BoundPropertySubpatternMember? member = node.Member; + BoundPattern pattern = RewritePatternWithUnionMatchingToPropertyPattern((BoundPattern)this.Visit(node.Pattern)); + return node.Update(member, node.IsLengthOrCount, pattern); + } + + public override BoundNode? VisitBinaryPattern(BoundBinaryPattern node) + { + var binaryPatternStack = ArrayBuilder.GetInstance(); + BoundBinaryPattern? currentNode = node; + + do + { + binaryPatternStack.Push(currentNode); + currentNode = currentNode.Left as BoundBinaryPattern; + } while (currentNode != null); + + Debug.Assert(binaryPatternStack.Count > 0); + + var binaryPattern = binaryPatternStack.Pop(); + BoundPattern result = (BoundPattern)Visit(binaryPattern.Left); +#if DEBUG + var narrowedTypeCandidates = ArrayBuilder.GetInstance(2); + + if (result is BoundPatternWithUnionMatching unionPattern) + { + narrowedTypeCandidates.Add(getDisjunctionType(unionPattern)); + } + else + { + Binder.CollectDisjunctionTypes(result, narrowedTypeCandidates, hasUnionMatching: false); + } +#endif + do + { + result = rewriteBinaryPattern( + this, + result, + binaryPattern +#if DEBUG + , narrowedTypeCandidates +#endif + ); + } + while (binaryPatternStack.TryPop(out binaryPattern)); + + binaryPatternStack.Free(); +#if DEBUG + narrowedTypeCandidates.Free(); +#endif + return result; + + static BoundPattern rewriteBinaryPattern( + UnionMatchingRewriter rewriter, + BoundPattern preboundLeft, + BoundBinaryPattern node +#if DEBUG + , ArrayBuilder narrowedTypeCandidates +#endif + ) + { + if (node.Disjunction) + { + preboundLeft = RewritePatternWithUnionMatchingToPropertyPattern(preboundLeft); + var right = RewritePatternWithUnionMatchingToPropertyPattern((BoundPattern)rewriter.Visit(node.Right)); + +#if DEBUG + // Here we are verifying that the narrowed type computed during the initial binding phase in + // 'Binder.BindBinaryPattern.bindBinaryPattern' matches what we compute here + // with all recursive patterns in place. So, if the algorithm changes there, we might need to + // update it here as well. However, we are trying to share the same helpers in both places as mach + // as possible. + + // Compute the common type. This algorithm is quadratic, but disjunctive patterns are unlikely to be huge + Binder.CollectDisjunctionTypes(right, narrowedTypeCandidates, hasUnionMatching: false); + var discardedSiteInfo = CompoundUseSiteInfo.Discarded; + TypeSymbol? leastSpecific = Binder.LeastSpecificType(narrowedTypeCandidates, rewriter._compilation.Conversions, ref discardedSiteInfo); + Debug.Assert(node.NarrowedType.Equals(leastSpecific ?? node.InputType, TypeCompareKind.ConsiderEverything)); +#endif + + return node.Update(disjunction: true, preboundLeft, right, inputType: node.InputType, narrowedType: node.NarrowedType); + + } + else + { + var right = (BoundPattern)rewriter.Visit(node.Right); + + BoundPattern result = makeConjunction(node.Syntax, preboundLeft, right, makeCompilerGenerated: node.WasCompilerGenerated); + +#if DEBUG + narrowedTypeCandidates.Clear(); + narrowedTypeCandidates.Add(result is BoundPatternWithUnionMatching unionResult ? getDisjunctionType(unionResult) : result.NarrowedType); +#endif + + return result; + } + + // If left and right are not BoundPatternWithUnionMatching, simply produce a regular BoundBinaryPattern representing the conjunction. + // Otherwise, create a BoundPatternWithUnionMatching representing the conjunction with the following properties: + // - There are no BoundPatternWithUnionMatching nodes under any ValuePattern. + // - The last union matching in evaluation order is always at the top + // - A preceding BoundPatternWithUnionMatching in evaluation order, if any, is the BoundPatternWithUnionMatching.LeftOfPendingConjunction. + static BoundPattern makeConjunction(SyntaxNode node, BoundPattern left, BoundPattern? right, bool makeCompilerGenerated) + { + if (right is BoundPatternWithUnionMatching rightUnionPattern) + { + // Update LeftOfPendingConjunction with the conjunction of left and LeftOfPendingConjunction + + // The code below unwraps the following recursive operation: + // return new BoundPatternWithUnionMatching( + // syntax: node, + // rightUnionPattern.UnionType, + // makeConjunction(node, left, rightUnionPattern.LeftOfPendingConjunction, makeCompilerGenerated: true), + // rightUnionPattern.ValueProperty, + // rightUnionPattern.ValuePattern, + // inputType: left.InputType).MakeCompilerGenerated(); + + var stack = ArrayBuilder.GetInstance(); + + stack.Push(rightUnionPattern); + + while (rightUnionPattern.LeftOfPendingConjunction is BoundPatternWithUnionMatching other) + { + stack.Push(other); + rightUnionPattern = other; + } + + Debug.Assert(rightUnionPattern.LeftOfPendingConjunction is not BoundPatternWithUnionMatching); + var leftOfPendingConjunction = makeConjunction(node, left, rightUnionPattern.LeftOfPendingConjunction, makeCompilerGenerated: true); + + do + { + rightUnionPattern = stack.Pop(); + leftOfPendingConjunction = new BoundPatternWithUnionMatching( + syntax: node, + rightUnionPattern.UnionMatchingInputType, + leftOfPendingConjunction, + rightUnionPattern.ValueProperty, + rightUnionPattern.ValuePattern, + inputType: left.InputType).MakeCompilerGenerated(); + } + while (!stack.IsEmpty); + + stack.Free(); + + return leftOfPendingConjunction; + } + else if (right is { }) + { + if (left is BoundPatternWithUnionMatching leftUnionPattern) + { + // The right is just a continuation of the ValuePattern. + // Update ValuePattern with the conjunction of ValuePattern and right, + // since neither of them contain union patterns, we can simply create a BoundBinaryPattern for that. + return new BoundPatternWithUnionMatching( + syntax: node, + leftUnionPattern.UnionMatchingInputType, + leftUnionPattern.LeftOfPendingConjunction, + leftUnionPattern.ValueProperty, + MakeBinaryAnd(node, leftUnionPattern.ValuePattern, right, makeCompilerGenerated), + inputType: leftUnionPattern.InputType).MakeCompilerGenerated(); + } + else + { + // Neither left nor right contain union patterns, create a BoundBinaryPattern for that. + return MakeBinaryAnd(node, left, right, makeCompilerGenerated); + } + } + else + { + return left; + } + } + } + +#if DEBUG + static TypeSymbol getDisjunctionType(BoundPatternWithUnionMatching unionPattern) + { + // Disjunction type is the UnionType for the first BoundPatternWithUnionMatching in evaluation order. + // That type won't be narrowed more for the purposes of a possible upcoming disjunction, since + // everything after that goes into a subputtern of a recursive pattern. + while (unionPattern.LeftOfPendingConjunction is BoundPatternWithUnionMatching leftUnionPattern) + { + unionPattern = leftUnionPattern; + } + + return unionPattern.UnionMatchingInputType; + } +#endif + } + + private static BoundBinaryPattern MakeBinaryAnd(SyntaxNode node, BoundPattern left, BoundPattern right, bool makeCompilerGenerated) + { + return new BoundBinaryPattern(node, disjunction: false, left, right, inputType: left.InputType, narrowedType: right.NarrowedType) { WasCompilerGenerated = makeCompilerGenerated }; + } + + private static BoundPattern RewritePatternWithUnionMatchingToPropertyPattern(BoundPattern pattern) + { + // If pattern contains BoundPatternWithUnionMatching pending a rewrite, we should have BoundPatternWithUnionMatching + // at the top. + if (pattern is BoundPatternWithUnionMatching unionPattern) + { + // If this method is called, we are sure that no more conjunctions will follow this pattern immediately. + // Therefore, no additional patterns are coming for the top most Value property. We can start rewriting + // BoundPatternWithUnionMatching from the top down, converting them to appropriate BoundRecursivePatterns and nesting + // them as we go down the chain. Effectively, we will end up with a chain of BoundRecursivePatterns in + // reversed order, i.e. BoundRecursivePatterns corresponding to the top-most BoundPatternWithUnionMatching will be + // at the bottom, and BoundRecursivePatterns corresponding to the bottom-most BoundPatternWithUnionMatching will be + // at the top. + + TypeSymbol unionMatchingInputType = unionPattern.UnionMatchingInputType; + BoundPropertySubpatternMember valueProperty = unionPattern.ValueProperty; + BoundPattern? leftOfPendingConjunction = unionPattern.LeftOfPendingConjunction; + BoundPattern valuePattern = unionPattern.ValuePattern; + + while (true) + { + var unionType = unionMatchingInputType.StrippedType(); + + BoundPattern result = new BoundRecursivePattern( + syntax: valuePattern.Syntax, + declaredType: null, + deconstructMethod: null, + deconstruction: default, + properties: [new BoundPropertySubpattern(valuePattern.Syntax, valueProperty, isLengthOrCount: false, valuePattern).MakeCompilerGenerated()], + variable: null, + variableAccess: null, + isExplicitNotNullTest: false, + isUnionMatching: false, + inputType: unionType, + narrowedType: unionType).MakeCompilerGenerated(); + + if (unionMatchingInputType.IsNullableType()) + { + // Prepend the 'Value' property pattern with a type pattern unwrapping the nullable value. + result = MakeBinaryAnd( + result.Syntax, + new BoundTypePattern( + result.Syntax, + declaredType: new BoundTypeExpression(result.Syntax, aliasOpt: null, unionType).MakeCompilerGenerated(), + isExplicitNotNullTest: false, // https://github.com/dotnet/roslyn/issues/82636: Is passing 'true' going to make a difference? + isUnionMatching: false, + inputType: unionMatchingInputType, + narrowedType: unionType).MakeCompilerGenerated(), + result, + makeCompilerGenerated: true); + } + + if (leftOfPendingConjunction is BoundPatternWithUnionMatching leftUnionPattern) + { + unionMatchingInputType = leftUnionPattern.UnionMatchingInputType; + valueProperty = leftUnionPattern.ValueProperty; + leftOfPendingConjunction = leftUnionPattern.LeftOfPendingConjunction; + valuePattern = MakeBinaryAnd(pattern.Syntax, leftUnionPattern.ValuePattern, result, makeCompilerGenerated: true); + + continue; + } + else if (leftOfPendingConjunction is { } left) + { + result = MakeBinaryAnd(pattern.Syntax, left, result, makeCompilerGenerated: true); + } + + Debug.Assert(result.InputType.Equals(pattern.InputType, TypeCompareKind.AllIgnoreOptions)); + return result; + } + } + + return pattern; + } + } +} diff --git a/src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs b/src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs index cd4480373251..b3a5f5e8d096 100644 --- a/src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/UsingStatementBinder.cs @@ -250,6 +250,14 @@ bool bindDisposable(bool fromExpression, out MethodArgumentInfo? patternDisposeI { awaitableType = originalBinder.Compilation.GetWellKnownType(WellKnownType.System_Threading_Tasks_ValueTask); } + else + { + var disposeMethod = originalBinder.Compilation.GetSpecialTypeMember(SpecialMember.System_IDisposable__Dispose); + if (disposeMethod != null) + { + originalBinder.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, disposeMethod, syntax); + } + } return !ReportUseSite(disposableInterface, diagnostics, hasAwait ? awaitKeyword : usingKeyword); } diff --git a/src/Compilers/CSharp/Portable/Binder/WithUsingNamespacesAndTypesBinder.cs b/src/Compilers/CSharp/Portable/Binder/WithUsingNamespacesAndTypesBinder.cs index 7a2ffd9a446e..8820fae13ffa 100644 --- a/src/Compilers/CSharp/Portable/Binder/WithUsingNamespacesAndTypesBinder.cs +++ b/src/Compilers/CSharp/Portable/Binder/WithUsingNamespacesAndTypesBinder.cs @@ -71,65 +71,7 @@ internal override bool SupportsExtensions get { return true; } } - internal override void GetCandidateExtensionMethodsInSingleBinder( - ArrayBuilder methods, - string name, - int arity, - LookupOptions options, - Binder originalBinder) - { - Debug.Assert(methods.Count == 0); - - bool callerIsSemanticModel = originalBinder.IsSemanticModelBinder; - - // We need to avoid collecting multiple candidates for an extension method imported both through a namespace and a static class - // We will look for duplicates only if both of the following flags are set to true - bool seenNamespaceWithExtensionMethods = false; - bool seenStaticClassWithExtensionMethods = false; - - foreach (var nsOrType in this.GetUsings(basesBeingResolved: null)) - { - switch (nsOrType.NamespaceOrType.Kind) - { - case SymbolKind.Namespace: - { - var count = methods.Count; - ((NamespaceSymbol)nsOrType.NamespaceOrType).GetExtensionMethods(methods, name, arity, options); - - // If we found any extension methods, then consider this using as used. - if (methods.Count != count) - { - MarkImportDirective(nsOrType.UsingDirectiveReference, callerIsSemanticModel); - seenNamespaceWithExtensionMethods = true; - } - - break; - } - - case SymbolKind.NamedType: - { - var count = methods.Count; - ((NamedTypeSymbol)nsOrType.NamespaceOrType).GetExtensionMethods(methods, name, arity, options); - - // If we found any extension methods, then consider this using as used. - if (methods.Count != count) - { - MarkImportDirective(nsOrType.UsingDirectiveReference, callerIsSemanticModel); - seenStaticClassWithExtensionMethods = true; - } - - break; - } - } - } - - if (seenNamespaceWithExtensionMethods && seenStaticClassWithExtensionMethods) - { - methods.RemoveDuplicates(); - } - } - - internal override void GetCandidateExtensionMembersInSingleBinder(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, Binder originalBinder) + internal override void GetAllExtensionCandidatesInSingleBinder(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, Binder originalBinder) { Debug.Assert(members.Count == 0); @@ -145,7 +87,7 @@ internal override void GetCandidateExtensionMembersInSingleBinder(ArrayBuilder obj is BoundDagEvaluation other && this.Equals(other); - public bool Equals(BoundDagEvaluation other) + public virtual bool Equals(BoundDagEvaluation other) { - return this == other || - this.IsEquivalentTo(other) && - this.Input.Equals(other.Input); + if (DecisionDagBuilder.IsEqualEvaluation(this, other)) + { + Debug.Assert(other.GetHashCode() == this.GetHashCode()); + Debug.Assert(DecisionDagBuilder.IsEqualEvaluation(other, this)); + Debug.Assert(other is BoundDagIndexerEvaluation or BoundDagTypeEvaluation or BoundDagPassThroughEvaluation || + this is BoundDagIndexerEvaluation or BoundDagTypeEvaluation or BoundDagPassThroughEvaluation || + other.Input.Equals(this.Input)); + return true; + } + + return false; + } + + public override int GetHashCode() + { + return Hash.Combine((int)Kind, this.Symbol?.GetHashCode() ?? 0); } /// @@ -41,11 +57,11 @@ private Symbol? Symbol BoundDagIndexEvaluation e => e.Property, BoundDagSliceEvaluation e => getSymbolFromIndexerAccess(e.IndexerAccess), BoundDagIndexerEvaluation e => getSymbolFromIndexerAccess(e.IndexerAccess), - BoundDagAssignmentEvaluation => null, + BoundDagAssignmentEvaluation or BoundDagPassThroughEvaluation => null, _ => throw ExceptionUtilities.UnexpectedValue(this.Kind) }; - Debug.Assert(result is not null || this is BoundDagAssignmentEvaluation); + Debug.Assert(result is not null || this is BoundDagAssignmentEvaluation or BoundDagPassThroughEvaluation); return result; static Symbol? getSymbolFromIndexerAccess(BoundExpression indexerAccess) @@ -67,9 +83,15 @@ private Symbol? Symbol } } - public override int GetHashCode() + public sealed override BoundDagTest UpdateTestImpl(BoundDagTemp input) => Update(input); + + public abstract BoundDagTemp MakeResultTemp(); + public new BoundDagEvaluation Update(BoundDagTemp input) => UpdateEvaluationImpl(input); + public abstract BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input); + + public virtual OneOrMany AllOutputs() { - return Hash.Combine(Input.GetHashCode(), this.Symbol?.GetHashCode() ?? 0); + return new OneOrMany(MakeResultTemp()); } #if DEBUG @@ -106,6 +128,61 @@ internal string GetOutputTempDebuggerDisplay() #endif } + partial class BoundDagTypeEvaluation + { + public override BoundDagTemp MakeResultTemp() + { + return new BoundDagTemp(Syntax, Type, this); + } + + public override BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input) => Update(input); + public new BoundDagTypeEvaluation Update(BoundDagTemp input) + { + return Update(Type, input); + } + + public override int GetHashCode() + { + BoundDagEvaluation? nonTypeEvaluation = DecisionDagBuilder.SkipAllTypeEvaluations(this); + + if (DecisionDagBuilder.IsUnionTryGetValueEvaluation(nonTypeEvaluation, out _, out BoundDagTemp? unionInstance) || + DecisionDagBuilder.IsUnionValueEvaluation(nonTypeEvaluation, out unionInstance)) + { + return unionInstance.GetHashCode(); + } + + return nonTypeEvaluation?.GetHashCode() ?? 0; + } + } + + partial class BoundDagFieldEvaluation + { + public override BoundDagTemp MakeResultTemp() + { + return new BoundDagTemp(Syntax, Field.Type, this); + } + + public override BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input) => Update(input); + public new BoundDagFieldEvaluation Update(BoundDagTemp input) + { + return Update(Field, input); + } + } + + partial class BoundDagPropertyEvaluation + { + public override BoundDagTemp MakeResultTemp() + { + return new BoundDagTemp(Syntax, Property.Type, this); + } + + public override BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input) => Update(input); + public new BoundDagPropertyEvaluation Update(BoundDagTemp input) + { + return Update(Property, IsLengthOrCount, input); + } + } + partial class BoundDagIndexEvaluation { public override int GetHashCode() => base.GetHashCode() ^ this.Index; @@ -115,11 +192,28 @@ public override bool IsEquivalentTo(BoundDagEvaluation obj) // base.IsEquivalentTo checks the kind field, so the following cast is safe this.Index == ((BoundDagIndexEvaluation)obj).Index; } + + public override BoundDagTemp MakeResultTemp() + { + Debug.Assert(Property.Type.IsObjectType()); + return new BoundDagTemp(Syntax, Property.Type, this); + } + + public override BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input) => Update(input); + public new BoundDagIndexEvaluation Update(BoundDagTemp input) + { + return Update(Property, Index, input); + } } partial class BoundDagIndexerEvaluation { - public override int GetHashCode() => base.GetHashCode() ^ this.Index; + public override int GetHashCode() + { + var (input, _, index) = DecisionDagBuilder.GetCanonicalInput(this); + return Hash.Combine(DecisionDagBuilder.OriginalInput(input), Hash.Combine((int)Kind, index)); + } + public override bool IsEquivalentTo(BoundDagEvaluation obj) { return base.IsEquivalentTo(obj) && @@ -130,6 +224,27 @@ private partial void Validate() { Debug.Assert(IndexerAccess is BoundIndexerAccess or BoundImplicitIndexerAccess or BoundArrayAccess); } + + public override BoundDagTemp MakeResultTemp() + { + return new BoundDagTemp(Syntax, IndexerType, this); + } + + public override BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input) => Update(input); + public new BoundDagIndexerEvaluation Update(BoundDagTemp input) + { + return Update(IndexerType, LengthTemp, Index, IndexerAccess, ReceiverPlaceholder, ArgumentPlaceholder, input); + } + + public BoundDagIndexerEvaluation Update(BoundDagTemp lengthTemp, BoundDagTemp input) + { + return Update(IndexerType, lengthTemp, Index, IndexerAccess, ReceiverPlaceholder, ArgumentPlaceholder, input); + } + + public override OneOrMany AllInputs() + { + return new OneOrMany([Input, LengthTemp]); + } } partial class BoundDagSliceEvaluation @@ -146,6 +261,27 @@ private partial void Validate() { Debug.Assert(IndexerAccess is BoundIndexerAccess or BoundImplicitIndexerAccess or BoundArrayAccess); } + + public override BoundDagTemp MakeResultTemp() + { + return new BoundDagTemp(Syntax, SliceType, this); + } + + public override BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input) => Update(input); + public new BoundDagSliceEvaluation Update(BoundDagTemp input) + { + return Update(SliceType, LengthTemp, StartIndex, EndIndex, IndexerAccess, ReceiverPlaceholder, ArgumentPlaceholder, input); + } + + public BoundDagSliceEvaluation Update(BoundDagTemp lengthTemp, BoundDagTemp input) + { + return Update(SliceType, lengthTemp, StartIndex, EndIndex, IndexerAccess, ReceiverPlaceholder, ArgumentPlaceholder, input); + } + + public override OneOrMany AllInputs() + { + return new OneOrMany([Input, LengthTemp]); + } } partial class BoundDagAssignmentEvaluation @@ -156,5 +292,102 @@ public override bool IsEquivalentTo(BoundDagEvaluation obj) return base.IsEquivalentTo(obj) && this.Target.Equals(((BoundDagAssignmentEvaluation)obj).Target); } + + public override bool Equals(BoundDagEvaluation other) + { + return this == other || + other is BoundDagAssignmentEvaluation assignment && + this.Target.Equals(assignment.Target) && + this.Input.Equals(assignment.Input); + } + + public override BoundDagTemp MakeResultTemp() + { + throw ExceptionUtilities.Unreachable(); + } + + public override BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input) => Update(input); + public new BoundDagAssignmentEvaluation Update(BoundDagTemp input) + { + return Update(Target, input); + } + } + + partial class BoundDagDeconstructEvaluation + { + public ArrayBuilder MakeOutParameterTemps() + { + MethodSymbol method = DeconstructMethod; + int extensionExtra = method.IsStatic ? 1 : 0; + int count = method.ParameterCount - extensionExtra; + var builder = ArrayBuilder.GetInstance(count); + for (int i = 0; i < count; i++) + { + ParameterSymbol parameter = method.Parameters[i + extensionExtra]; + Debug.Assert(parameter.RefKind == RefKind.Out); + builder.Add(new BoundDagTemp(Syntax, parameter.Type, this, i)); + } + + return builder; + } + + public BoundDagTemp MakeFirstOutParameterTemp() + { + MethodSymbol method = DeconstructMethod; + int extensionExtra = method.IsStatic ? 1 : 0; + ParameterSymbol parameter = method.Parameters[extensionExtra]; + Debug.Assert(parameter.RefKind == RefKind.Out); + return new BoundDagTemp(Syntax, parameter.Type, this, 0); + } + + public BoundDagTemp MakeReturnValueTemp() + { + Debug.Assert(!DeconstructMethod.ReturnsVoid); + return new BoundDagTemp(Syntax, DeconstructMethod.ReturnType, this, index: -1); + } + + public override BoundDagTemp MakeResultTemp() + { + throw ExceptionUtilities.Unreachable(); + } + + public override BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input) => Update(input); + public new BoundDagDeconstructEvaluation Update(BoundDagTemp input) + { + return Update(DeconstructMethod, input); + } + + public override OneOrMany AllOutputs() + { + var builder = MakeOutParameterTemps(); + if (!DeconstructMethod.ReturnsVoid) + { + builder.Add(MakeReturnValueTemp()); + } + + if (builder is [var one]) + { + builder.Free(); + return new OneOrMany(one); + } + + return new OneOrMany(builder.ToImmutableAndFree()); + } + } + + partial class BoundDagPassThroughEvaluation + { + public override int GetHashCode() + { + Debug.Assert(Input.Source is BoundDagTypeEvaluation); + return DecisionDagBuilder.NotTypeEvaluationInput(Input.Source).GetHashCode(); + } + + public override BoundDagTemp MakeResultTemp() + { + return Input; + } + + public override BoundDagEvaluation UpdateEvaluationImpl(BoundDagTemp input) => Update(input); } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundDagRelationalTest.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundDagRelationalTest.cs index df4bae7ddac4..21624a9b969c 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundDagRelationalTest.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundDagRelationalTest.cs @@ -7,5 +7,11 @@ namespace Microsoft.CodeAnalysis.CSharp internal sealed partial class BoundDagRelationalTest { public BinaryOperatorKind Relation => OperatorKind.Operator(); + + public override BoundDagTest UpdateTestImpl(BoundDagTemp input) => Update(input); + public new BoundDagRelationalTest Update(BoundDagTemp input) + { + return Update(OperatorKind, Value, input); + } } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundDagTemp.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundDagTemp.cs index 40e0907e66e2..ef2fa1fa6eb8 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundDagTemp.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundDagTemp.cs @@ -24,10 +24,14 @@ partial class BoundDagTemp public bool Equals(BoundDagTemp other) { - return - this.Type.Equals(other.Type, TypeCompareKind.AllIgnoreOptions) && - object.Equals(this.Source, other.Source) && - this.Index == other.Index; + if (IsEquivalentTo(other) && + Equals(other.Source, this.Source)) + { + Debug.Assert(other.GetHashCode() == this.GetHashCode()); + return true; + } + + return false; } /// @@ -60,7 +64,20 @@ public override int GetHashCode() null => "t0", var id => $"t{id}" }; - return $"{name}{(Source is BoundDagDeconstructEvaluation ? $".Item{(Index + 1).ToString()}" : "")}"; + + if (Source is BoundDagDeconstructEvaluation) + { + if (Index == -1) + { + return name + ".ReturnItem"; + } + else + { + return name + ".Item" + (Index + 1).ToString(); + } + } + + return name; } #endif } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundDagTest.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundDagTest.cs index b45c6ce190c5..1c40446ff723 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundDagTest.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundDagTest.cs @@ -4,6 +4,8 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.CSharp.Symbols; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp @@ -46,6 +48,14 @@ public override int GetHashCode() return Hash.Combine(((int)Kind).GetHashCode(), Input.GetHashCode()); } + public BoundDagTest Update(BoundDagTemp input) => UpdateTestImpl(input); + public abstract BoundDagTest UpdateTestImpl(BoundDagTemp input); + + public virtual OneOrMany AllInputs() + { + return new OneOrMany(Input); + } + #if DEBUG internal new string GetDebuggerDisplay() { @@ -59,7 +69,14 @@ public override int GetHashCode() return $"{e.GetOutputTempDebuggerDisplay()} = {e.Input.GetDebuggerDisplay()}.{e.Field.Name}"; case BoundDagDeconstructEvaluation d: var result = "("; + + if (DecisionDagBuilder.IsUnionTryGetValueEvaluation(d, out TypeSymbol? targetType, out _)) + { + result = $"TryGetValue({targetType}): " + result; + } + var first = true; + foreach (var param in d.DeconstructMethod.Parameters) { if (!first) @@ -69,6 +86,17 @@ public override int GetHashCode() first = false; result += $"Item{param.Ordinal + 1}"; } + + if (!d.DeconstructMethod.ReturnsVoid) + { + if (!first) + { + result += ", "; + } + + result += $"ReturnItem"; + } + result += $") {d.GetOutputTempDebuggerDisplay()} = {d.Input.GetDebuggerDisplay()}"; return result; case BoundDagIndexEvaluation i: @@ -77,6 +105,8 @@ public override int GetHashCode() return $"{i.GetOutputTempDebuggerDisplay()} = {i.Input.GetDebuggerDisplay()}[{i.Index}]"; case BoundDagAssignmentEvaluation i: return $"{i.Target.GetDebuggerDisplay()} <-- {i.Input.GetDebuggerDisplay()}"; + case BoundDagPassThroughEvaluation i: + return $"PassThrough {i.Input.GetDebuggerDisplay()}"; case BoundDagEvaluation e: return $"{e.GetOutputTempDebuggerDisplay()} = {e.Kind}({e.Input.GetDebuggerDisplay()})"; case BoundDagTypeTest b: @@ -104,4 +134,36 @@ public override int GetHashCode() } #endif } + + partial class BoundDagValueTest + { + public override BoundDagTest UpdateTestImpl(BoundDagTemp input) => Update(input); + public new BoundDagValueTest Update(BoundDagTemp input) + { + return Update(Value, input); + } + } + + partial class BoundDagExplicitNullTest + { + public override BoundDagTest UpdateTestImpl(BoundDagTemp input) => Update(input); + } + + partial class BoundDagNonNullTest + { + public override BoundDagTest UpdateTestImpl(BoundDagTemp input) => Update(input); + public new BoundDagNonNullTest Update(BoundDagTemp input) + { + return Update(IsExplicitTest, input); + } + } + + partial class BoundDagTypeTest + { + public override BoundDagTest UpdateTestImpl(BoundDagTemp input) => Update(input); + public new BoundDagTypeTest Update(BoundDagTemp input) + { + return Update(Type, input); + } + } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundDecisionDag.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundDecisionDag.cs index 181d6aad426b..50fadaaea58f 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundDecisionDag.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundDecisionDag.cs @@ -113,7 +113,7 @@ public BoundDecisionDag Rewrite(Func @@ -200,11 +200,6 @@ BoundDecisionDagNode makeReplacement(BoundDecisionDagNode dag, IReadOnlyDictiona } } - public bool ContainsAnySynthesizedNodes() - { - return this.TopologicallySortedNodes.Any(static node => node is BoundEvaluationDecisionDagNode e && e.Evaluation.Kind == BoundKind.DagAssignmentEvaluation); - } - #if DEBUG /// /// Starting with `this` state, produce a human-readable description of the state tables. diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs index 3c5ab45d8f19..889dac499bb9 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundExpression.cs @@ -233,7 +233,7 @@ public sealed override bool IsEquivalentToThisReference { get { - Debug.Assert(false); // Getting here is unexpected. + Debug.Fail("Getting here is unexpected."); return false; } } @@ -697,4 +697,16 @@ internal void VisitAllElements(Action action, T args) } } } + + internal partial class BoundValueForNullableAnalysis + { + public sealed override bool IsEquivalentToThisReference + { + get + { + Debug.Fail("Getting here is unexpected."); + return false; + } + } + } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundIsPatternExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundIsPatternExpression.cs index bab83be24fbf..57e81726e10b 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundIsPatternExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundIsPatternExpression.cs @@ -11,7 +11,7 @@ internal partial class BoundIsPatternExpression public BoundDecisionDag GetDecisionDagForLowering(CSharpCompilation compilation) { BoundDecisionDag decisionDag = this.ReachabilityDecisionDag; - if (decisionDag.ContainsAnySynthesizedNodes()) + if (!decisionDag.SuitableForLowering) { bool negated = this.Pattern.IsNegated(out var innerPattern); Debug.Assert(negated == this.IsNegated); @@ -20,11 +20,12 @@ public BoundDecisionDag GetDecisionDagForLowering(CSharpCompilation compilation) this.Syntax, this.Expression, innerPattern, + HasUnionMatching, this.WhenTrueLabel, this.WhenFalseLabel, BindingDiagnosticBag.Discarded, forLowering: true); - Debug.Assert(!decisionDag.ContainsAnySynthesizedNodes()); + Debug.Assert(decisionDag.SuitableForLowering); } return decisionDag; diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundListPattern.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundListPattern.cs index 0c913cafc255..a1b42d7fb53a 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundListPattern.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundListPattern.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using Microsoft.CodeAnalysis.CSharp.Symbols; namespace Microsoft.CodeAnalysis.CSharp @@ -12,15 +13,24 @@ internal partial class BoundListPattern { internal BoundListPattern WithSubpatterns(ImmutableArray subpatterns) { - return Update(subpatterns, this.HasSlice, this.LengthAccess, this.IndexerAccess, this.ReceiverPlaceholder, this.ArgumentPlaceholder, this.Variable, this.VariableAccess, this.InputType, this.NarrowedType); + return Update(subpatterns, this.HasSlice, this.LengthAccess, this.IndexerAccess, this.ReceiverPlaceholder, this.ArgumentPlaceholder, this.Variable, this.VariableAccess, this.IsUnionMatching, this.InputType, this.NarrowedType); } private partial void Validate() { + Debug.Assert(!Subpatterns.Any(p => p is BoundPatternWithUnionMatching)); Debug.Assert(LengthAccess is null or BoundPropertyAccess or BoundBadExpression); Debug.Assert(IndexerAccess is null or BoundIndexerAccess or BoundImplicitIndexerAccess or BoundArrayAccess or BoundBadExpression or BoundDynamicIndexerAccess or BoundPointerElementAccess); Debug.Assert(Binder.GetIndexerOrImplicitIndexerSymbol(IndexerAccess) is var _); - Debug.Assert(NarrowedType.Equals(InputType.StrippedType(), TypeCompareKind.AllIgnoreOptions)); + + if (IsUnionMatching) + { + Debug.Assert(NarrowedType.IsObjectType()); + } + else + { + Debug.Assert(NarrowedType.Equals(InputType.StrippedType(), TypeCompareKind.AllIgnoreOptions)); + } } } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNegatedPattern.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundNegatedPattern.cs index 412e783539c5..7283ef856d9e 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNegatedPattern.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNegatedPattern.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; namespace Microsoft.CodeAnalysis.CSharp { @@ -10,8 +11,18 @@ internal partial class BoundNegatedPattern { private partial void Validate() { - Debug.Assert(NarrowedType.Equals(InputType, TypeCompareKind.AllIgnoreOptions)); - Debug.Assert(Negated.InputType.Equals(InputType, TypeCompareKind.AllIgnoreOptions)); + if (IsUnionMatching) + { + Debug.Assert(NarrowedType.IsObjectType()); + Debug.Assert(Negated.InputType.IsObjectType()); + } + else + { + Debug.Assert(NarrowedType.Equals(InputType, TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(Negated.InputType.Equals(InputType, TypeCompareKind.AllIgnoreOptions)); + } + + Debug.Assert(Negated is not BoundPatternWithUnionMatching); } } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNode.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundNode.cs index a121970dfa9d..930bbfa7ba5b 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNode.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNode.cs @@ -454,6 +454,61 @@ public static Conversion GetConversion(BoundExpression? conversion, BoundValuePl return boundConversion.Conversion; } + ConversionGroup? conversionGroupOpt = boundConversion.ConversionGroupOpt; + if (conversionGroupOpt?.Conversion.IsUserDefined == true) + { + BoundConversion? possiblyUserDefined = boundConversion; + while (possiblyUserDefined?.Conversion.IsUserDefined == false) + { + possiblyUserDefined = possiblyUserDefined.Operand as BoundConversion; + } + + if (possiblyUserDefined is not null) + { + Debug.Assert(possiblyUserDefined.Conversion.IsUserDefined); + var operand = possiblyUserDefined.Operand; + + while (operand is BoundConversion operandAsConversion && operandAsConversion.ConversionGroupOpt == conversionGroupOpt) + { + operand = operandAsConversion.Operand; + } + + if ((object)operand == placeholder) + { + return possiblyUserDefined.Conversion; + } + } + + throw ExceptionUtilities.UnexpectedValue(conversion); + } + + if (conversionGroupOpt?.Conversion.IsUnion == true) // https://github.com/dotnet/roslyn/issues/82636: Add coverage + { + BoundConversion? possiblyUnion = boundConversion; + while (possiblyUnion?.Conversion.IsUnion == false) + { + possiblyUnion = possiblyUnion.Operand as BoundConversion; + } + + if (possiblyUnion is not null) + { + Debug.Assert(possiblyUnion.Conversion.IsUnion); + var operand = possiblyUnion.Operand; + + while (operand is BoundConversion operandAsConversion && operandAsConversion.ConversionGroupOpt == conversionGroupOpt) + { + operand = operandAsConversion.Operand; + } + + if ((object)operand == placeholder) + { + return possiblyUnion.Conversion; + } + } + + throw ExceptionUtilities.UnexpectedValue(conversion); + } + if (!boundConversion.Conversion.IsUserDefined) { boundConversion = (BoundConversion)boundConversion.Operand; @@ -461,6 +516,7 @@ public static Conversion GetConversion(BoundExpression? conversion, BoundValuePl if (boundConversion.Conversion.IsUserDefined) { + Debug.Assert((boundConversion.InConversionGroupFlags & InConversionGroupFlags.LoweredFormOfUserDefinedConversionForExpressionTree) != 0); BoundConversion next; if ((object)boundConversion.Operand == placeholder || diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml index 9696f575a7ff..b0228b702489 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundNodes.xml @@ -1523,6 +1523,7 @@ + @@ -1547,6 +1548,7 @@ + @@ -1594,7 +1596,7 @@ - + @@ -1671,6 +1673,9 @@ + + + @@ -1679,6 +1684,7 @@ + @@ -2411,21 +2417,60 @@ + - + - + + + + + + + + + + + + + @@ -2444,6 +2489,7 @@ https://github.com/dotnet/roslyn/issues/13960 . When that is fixed this field can be removed. --> + @@ -2452,7 +2498,10 @@ - + @@ -2461,7 +2510,10 @@ - + @@ -2502,9 +2554,10 @@ + - + @@ -2526,6 +2579,7 @@ + + + - + + @@ -2609,12 +2667,17 @@ - + + + + + + diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundPattern.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundPattern.cs index b8614e66b975..3bef9a123487 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundPattern.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundPattern.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; + namespace Microsoft.CodeAnalysis.CSharp { internal partial class BoundPattern @@ -15,12 +18,32 @@ internal bool IsNegated(out BoundPattern innerPattern) { innerPattern = this; bool negated = false; + + if (innerPattern is BoundNegatedPattern { IsUnionMatching: true }) + { + // This node doesn't represent a negation at the top level, it really represents a negation + // in a property sub-pattern. Therefore, doing a semantically equivalent unwrapping for such + // BoundNegatedPattern isn't trivial since we need to preserve the "union matching" piece stored + // in the node, probably by rewriting innerPattern.Negated. + // Bottom line, this will not be a trivial unwrapping. Since the unwrapping is not necessary for + // correctness, we simply don't do it. + return negated; + } + while (innerPattern is BoundNegatedPattern negatedPattern) { + Debug.Assert(!negatedPattern.IsUnionMatching); negated = !negated; innerPattern = negatedPattern.Negated; } return negated; } + + public virtual bool IsUnionMatching => false; + + private partial void Validate() + { + Debug.Assert(!IsUnionMatching || InputType is { IsSubjectForUnionMatching: true }); + } } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundPatternWithUnionMatching.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundPatternWithUnionMatching.cs new file mode 100644 index 000000000000..5c7b2c67da85 --- /dev/null +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundPatternWithUnionMatching.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; + +namespace Microsoft.CodeAnalysis.CSharp +{ + internal partial class BoundPatternWithUnionMatching + { + private partial void Validate() + { + Debug.Assert(UnionMatchingInputType.IsSubjectForUnionMatching); + Debug.Assert(InputType.Equals(LeftOfPendingConjunction?.InputType ?? UnionMatchingInputType, TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(UnionMatchingInputType == (object)(LeftOfPendingConjunction?.NarrowedType ?? InputType)); + Debug.Assert(NarrowedType == (object)ValuePattern.NarrowedType); + Debug.Assert(ValuePattern is not BoundPatternWithUnionMatching); + } + + public BoundPatternWithUnionMatching(SyntaxNode syntax, TypeSymbol unionType, BoundPropertySubpatternMember valueProperty, BoundPattern pattern, TypeSymbol inputType) + : this(syntax, unionType, leftOfPendingConjunction: null, valueProperty, pattern, inputType, pattern.NarrowedType) + { + } + + public BoundPatternWithUnionMatching(SyntaxNode syntax, TypeSymbol unionType, BoundPattern? leftOfPendingConjunction, BoundPropertySubpatternMember valueProperty, BoundPattern pattern, TypeSymbol inputType) + : this(syntax, unionType, leftOfPendingConjunction, valueProperty, pattern, inputType, pattern.NarrowedType) + { + } + } +} diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundRecursivePattern.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundRecursivePattern.cs index dfc478df4d92..4afe14a910c2 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundRecursivePattern.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundRecursivePattern.cs @@ -11,9 +11,22 @@ internal partial class BoundRecursivePattern { private partial void Validate() { - Debug.Assert(DeclaredType is null ? - NarrowedType.Equals(InputType.StrippedType(), TypeCompareKind.AllIgnoreOptions) : - NarrowedType.Equals(DeclaredType.Type, TypeCompareKind.AllIgnoreOptions)); + + if (DeclaredType is null) + { + if (IsUnionMatching) + { + Debug.Assert(NarrowedType.IsObjectType()); + } + else + { + Debug.Assert(NarrowedType.Equals(InputType.StrippedType(), TypeCompareKind.AllIgnoreOptions)); + } + } + else + { + Debug.Assert(NarrowedType.Equals(DeclaredType.Type, TypeCompareKind.AllIgnoreOptions)); + } } } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundRelationalPattern.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundRelationalPattern.cs index 955782d1edbe..3bb4c271eca5 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundRelationalPattern.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundRelationalPattern.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics; +using Microsoft.CodeAnalysis.CSharp.Symbols; namespace Microsoft.CodeAnalysis.CSharp { @@ -10,8 +11,16 @@ internal partial class BoundRelationalPattern { private partial void Validate() { - Debug.Assert(NarrowedType.Equals(InputType, TypeCompareKind.AllIgnoreOptions) || - NarrowedType.Equals(Value.Type, TypeCompareKind.AllIgnoreOptions)); + if (IsUnionMatching) + { + Debug.Assert(NarrowedType.IsObjectType() || + NarrowedType.Equals(Value.Type, TypeCompareKind.AllIgnoreOptions)); + } + else + { + Debug.Assert(NarrowedType.Equals(InputType, TypeCompareKind.AllIgnoreOptions) || + NarrowedType.Equals(Value.Type, TypeCompareKind.AllIgnoreOptions)); + } } } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundSlicePattern.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundSlicePattern.cs index 07ceaec240c5..91bb112bcd6b 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundSlicePattern.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundSlicePattern.cs @@ -15,6 +15,7 @@ internal BoundSlicePattern WithPattern(BoundPattern? pattern) private partial void Validate() { + Debug.Assert(Pattern is not BoundPatternWithUnionMatching); Debug.Assert(IndexerAccess is null or BoundIndexerAccess or BoundImplicitIndexerAccess or BoundArrayAccess or BoundBadExpression or BoundDynamicIndexerAccess); Debug.Assert(Binder.GetIndexerOrImplicitIndexerSymbol(IndexerAccess) is var _); Debug.Assert(NarrowedType.Equals(InputType, TypeCompareKind.AllIgnoreOptions)); diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundSubpattern.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundSubpattern.cs new file mode 100644 index 000000000000..09d151061251 --- /dev/null +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundSubpattern.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; + +namespace Microsoft.CodeAnalysis.CSharp +{ + internal partial class BoundSubpattern + { + private partial void Validate() + { + Debug.Assert(Pattern is not BoundPatternWithUnionMatching); + } + } +} diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundSwitchExpression.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundSwitchExpression.cs index d86b6cddf490..3942334f8714 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundSwitchExpression.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundSwitchExpression.cs @@ -14,7 +14,7 @@ public BoundDecisionDag GetDecisionDagForLowering(CSharpCompilation compilation, defaultLabel = this.DefaultLabel; BoundDecisionDag decisionDag = this.ReachabilityDecisionDag; - if (decisionDag.ContainsAnySynthesizedNodes()) + if (!decisionDag.SuitableForLowering) { decisionDag = DecisionDagBuilder.CreateDecisionDagForSwitchExpression( compilation, @@ -26,7 +26,7 @@ public BoundDecisionDag GetDecisionDagForLowering(CSharpCompilation compilation, defaultLabel ??= new GeneratedLabelSymbol("default"), BindingDiagnosticBag.Discarded, forLowering: true); - Debug.Assert(!decisionDag.ContainsAnySynthesizedNodes()); + Debug.Assert(decisionDag.SuitableForLowering); } return decisionDag; diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundSwitchStatement.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundSwitchStatement.cs index 5f009c96a3db..8d13c9efa83e 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundSwitchStatement.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundSwitchStatement.cs @@ -12,7 +12,7 @@ internal partial class BoundSwitchStatement public BoundDecisionDag GetDecisionDagForLowering(CSharpCompilation compilation) { BoundDecisionDag decisionDag = this.ReachabilityDecisionDag; - if (decisionDag.ContainsAnySynthesizedNodes()) + if (!decisionDag.SuitableForLowering) { decisionDag = DecisionDagBuilder.CreateDecisionDagForSwitchStatement( compilation, @@ -22,7 +22,7 @@ public BoundDecisionDag GetDecisionDagForLowering(CSharpCompilation compilation) this.DefaultLabel?.Label ?? this.BreakLabel, BindingDiagnosticBag.Discarded, forLowering: true); - Debug.Assert(!decisionDag.ContainsAnySynthesizedNodes()); + Debug.Assert(decisionDag.SuitableForLowering); } return decisionDag; diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundTreeVisitors.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundTreeVisitors.cs index 397ded769211..53dfa6c0b359 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundTreeVisitors.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundTreeVisitors.cs @@ -224,6 +224,7 @@ protected BoundNode VisitExpressionOrPatternWithStackGuard(ref int recursionDept return result; } + [DebuggerStepThrough] protected virtual void EnsureSufficientExecutionStack(int recursionDepth) { StackGuard.EnsureSufficientExecutionStack(recursionDepth); diff --git a/src/Compilers/CSharp/Portable/BoundTree/ConversionGroup.cs b/src/Compilers/CSharp/Portable/BoundTree/ConversionGroup.cs index 4fcab50e53d2..e336ff07411b 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/ConversionGroup.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/ConversionGroup.cs @@ -68,12 +68,22 @@ internal enum InConversionGroupFlags : ushort TupleLiteralExplicitIdentity = 1 << 2, FunctionTypeDelegate = 1 << 3, FunctionTypeDelegateToTarget = 1 << 4, + UserDefinedFromConversion = 1 << 5, UserDefinedFromConversionAdjustment = 1 << 6, UserDefinedOperator = 1 << 7, UserDefinedReturnTypeAdjustment = 1 << 8, UserDefinedFinal = 1 << 9, UserDefinedErroneous = 1 << 10, + + UserDefinedAllFlags = UserDefinedOperator | UserDefinedFromConversion | UserDefinedFromConversionAdjustment | UserDefinedReturnTypeAdjustment | UserDefinedFinal | UserDefinedErroneous, + TupleBinaryOperatorPendingLowering = 1 << 11, + + UnionSourceConversion = 1 << 12, + UnionConstructor = 1 << 13, + UnionFinal = 1 << 14, + + UnionAllFlags = UnionConstructor | UnionSourceConversion | UnionFinal, } } diff --git a/src/Compilers/CSharp/Portable/BoundTree/UnboundLambda.cs b/src/Compilers/CSharp/Portable/BoundTree/UnboundLambda.cs index d680f26ce798..d1a6a90c8783 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/UnboundLambda.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/UnboundLambda.cs @@ -836,11 +836,17 @@ private BoundLambda ReallyBind(NamedTypeSymbol delegateType, bool inExpressionTr // We don't reuse the body if we're binding in an expression tree, because we didn't // know that we were binding for an expression tree when originally binding the lambda // for return inference. + // We also don't reuse the body if the value of CSharpCompilation.IsRuntimeAsyncEnabledInMethod + // changed after inference with the final return type, as that will change the binding of await + // calls within the lambda. For optimization purposes, we assume that if the lambda is async, it will + // end up resolving to Task/ValueTask types, as that's the 95% case for C# code. If that ends up not being + // true, then we need to bust the cache and rebind. if (!inExpressionTree && refKind == CodeAnalysis.RefKind.None && _returnInferenceCache!.TryGetValue(cacheKey, out BoundLambda? returnInferenceLambda) && GetLambdaExpressionBody(returnInferenceLambda.Body) is BoundExpression expression && (lambdaSymbol = (LambdaSymbol)returnInferenceLambda.Symbol).RefKind == refKind && + !lambdaSymbol.RuntimeAsyncEnabledChangedDuringInference && (object)LambdaSymbol.InferenceFailureReturnType != lambdaSymbol.ReturnType && lambdaSymbol.ReturnTypeWithAnnotations.Equals(returnType, TypeCompareKind.ConsiderEverything) && lambdaSymbol.RefCustomModifiers.SequenceEqual(refCustomModifiers)) @@ -966,7 +972,7 @@ private void ValidateUnsafeParameters(BindingDiagnosticBag diagnostics, Immutabl { if (targetParameterTypes[i].Type.ContainsPointerOrFunctionPointer()) { - this.Binder.ReportUnsafeIfNotAllowed(this.ParameterLocation(i), diagnostics); + this.Binder.ReportUnsafeIfNotAllowed(this.ParameterLocation(i), diagnostics, disallowedUnder: MemorySafetyRules.Legacy); } } } diff --git a/src/Compilers/CSharp/Portable/CSharpCompilationOptions.cs b/src/Compilers/CSharp/Portable/CSharpCompilationOptions.cs index b105d14ff810..795c9c37b407 100644 --- a/src/Compilers/CSharp/Portable/CSharpCompilationOptions.cs +++ b/src/Compilers/CSharp/Portable/CSharpCompilationOptions.cs @@ -22,11 +22,20 @@ namespace Microsoft.CodeAnalysis.CSharp public sealed class CSharpCompilationOptions : CompilationOptions, IEquatable #pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode(). { + // When adding new fields/properties, you should update + // Equals, GetHashCode, CSharpDeterministicKeyBuilder, and the following tests: + // - CSharpCompilationOptionsTests.TestFieldsForEqualsAndGetHashCode + // - CSharpDeterministicKeyBuilderTests.CSharpCompilationOptionsCombination + // - CSharpDeterministicKeyBuilderTests.VerifyUpToDate + /// /// Allow unsafe regions (i.e. unsafe modifiers on members and unsafe blocks). /// public bool AllowUnsafe { get; private set; } + // https://github.com/dotnet/roslyn/issues/82546: public API + internal int MemorySafetyRules { get; private set; } + /// /// Global namespace usings. /// @@ -262,6 +271,8 @@ private CSharpCompilationOptions(CSharpCompilationOptions other) : this( topLevelBinderFlags: other.TopLevelBinderFlags, nullableContextOptions: other.NullableContextOptions) { + // https://github.com/dotnet/roslyn/issues/82546: should be in the constructor + MemorySafetyRules = other.MemorySafetyRules; } public override string Language => LanguageNames.CSharp; @@ -420,6 +431,25 @@ public CSharpCompilationOptions WithAllowUnsafe(bool enabled) return new CSharpCompilationOptions(this) { AllowUnsafe = enabled }; } + // https://github.com/dotnet/roslyn/issues/82546: public API + internal CSharpCompilationOptions WithMemorySafetyRules(int version) + { + if (version == this.MemorySafetyRules) + { + return this; + } + + return new CSharpCompilationOptions(this) { MemorySafetyRules = version }; + } + + // https://github.com/dotnet/roslyn/issues/82546: determine what the "updated" number should be + internal const int UpdatedMemorySafetyRulesVersion = 2; + + internal CSharpCompilationOptions WithUpdatedMemorySafetyRules(bool enabled = true) + => WithMemorySafetyRules(enabled ? UpdatedMemorySafetyRulesVersion : 0); + + internal bool UseUpdatedMemorySafetyRules => MemorySafetyRules >= UpdatedMemorySafetyRulesVersion; + public new CSharpCompilationOptions WithPlatform(Platform platform) { if (this.Platform == platform) @@ -722,6 +752,8 @@ internal override void ValidateOptions(ArrayBuilder builder) builder.Add(Diagnostic.Create(MessageProvider.Instance, (int)ErrorCode.ERR_BadCompilationOptionValue, nameof(MetadataImportOptions), MetadataImportOptions.ToString())); } + // https://github.com/dotnet/roslyn/issues/82546: validate the value of MemorySafetyRules? + // TODO: add check for // (kind == 'arm' || kind == 'appcontainer' || kind == 'winmdobj') && // (version >= "6.2") @@ -740,6 +772,7 @@ public bool Equals(CSharpCompilationOptions? other) } return this.AllowUnsafe == other.AllowUnsafe && + this.MemorySafetyRules == other.MemorySafetyRules && this.TopLevelBinderFlags == other.TopLevelBinderFlags && (this.Usings == null ? other.Usings == null : this.Usings.SequenceEqual(other.Usings, StringComparer.Ordinal) && this.NullableContextOptions == other.NullableContextOptions); @@ -753,9 +786,10 @@ public override bool Equals(object? obj) protected override int ComputeHashCode() { return Hash.Combine(GetHashCodeHelper(), + Hash.Combine(this.MemorySafetyRules, Hash.Combine(this.AllowUnsafe, Hash.Combine(Hash.CombineValues(this.Usings, StringComparer.Ordinal), - Hash.Combine(((uint)TopLevelBinderFlags).GetHashCode(), ((int)this.NullableContextOptions).GetHashCode())))); + Hash.Combine(((uint)TopLevelBinderFlags).GetHashCode(), ((int)this.NullableContextOptions).GetHashCode()))))); } internal override Diagnostic? FilterDiagnostic(Diagnostic diagnostic, CancellationToken cancellationToken) diff --git a/src/Compilers/CSharp/Portable/CSharpResources.resx b/src/Compilers/CSharp/Portable/CSharpResources.resx index eb0acc8d91e7..5ef638e19f10 100644 --- a/src/Compilers/CSharp/Portable/CSharpResources.resx +++ b/src/Compilers/CSharp/Portable/CSharpResources.resx @@ -1153,7 +1153,7 @@ Missing partial modifier on declaration of type '{0}'; another partial declaration of this type exists - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces Partial declarations of '{0}' have conflicting accessibility modifiers @@ -4755,7 +4755,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -5795,7 +5795,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. - + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. @@ -7441,8 +7441,8 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. Target runtime doesn't support ref fields. @@ -8234,10 +8234,84 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ Operator resolution is ambiguous between the following members: '{0}' and '{1}' + + updated memory safety rules + + + This operation may only be used in an unsafe context + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + + + Unsafe member '{0}' cannot override safe member '{1}' + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + + + Unsafe member '{0}' cannot implement safe member '{1}' + + + RequiresUnsafeAttribute cannot be applied to this symbol. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + 'RequiresUnsafeAttribute' is not localizable + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 'unsafe' is a keyword, should not be localized. + '{0}' does not contain a definition for '{1}' and no accessible extension method '{1}' accepting a first argument of type '{0}' could be found (did you mean to iterate over the async collection with 'await foreach' instead?) 'await foreach' is not localizable + + An expression tree may not contain a union conversion. + + + unions + + + A union declaration must specify at least one case type. + + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + closed classes diff --git a/src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs b/src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs index f9e9eed097a8..844d9cae2ad4 100644 --- a/src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs +++ b/src/Compilers/CSharp/Portable/CodeGen/CodeGenerator.cs @@ -329,7 +329,9 @@ private void HandleReturn() && _module.Compilation.IsRuntimeAsyncEnabledIn(_method) && ((InternalSpecialType)_method.ReturnType.ExtendedSpecialType) is InternalSpecialType.System_Threading_Tasks_Task or InternalSpecialType.System_Threading_Tasks_ValueTask)); - if (_emitPdbSequencePoints && !_method.IsIterator && !_method.IsAsync) + if (_emitPdbSequencePoints && + !_method.IsIterator && + (!_method.IsAsync || _module.Compilation.IsRuntimeAsyncEnabledIn(_method))) { // In debug mode user could set a breakpoint on the last "}" of the method and // expect to hit it before exiting the method. diff --git a/src/Compilers/CSharp/Portable/CodeGen/EmitConversion.cs b/src/Compilers/CSharp/Portable/CodeGen/EmitConversion.cs index ba0fef936a98..995ccc35be2f 100644 --- a/src/Compilers/CSharp/Portable/CodeGen/EmitConversion.cs +++ b/src/Compilers/CSharp/Portable/CodeGen/EmitConversion.cs @@ -121,6 +121,7 @@ private void EmitConversion(BoundConversion conversion) break; case ConversionKind.ImplicitUserDefined: case ConversionKind.ExplicitUserDefined: + case ConversionKind.Union: case ConversionKind.AnonymousFunction: case ConversionKind.MethodGroup: case ConversionKind.ImplicitTupleLiteral: diff --git a/src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs b/src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs index 706ef0d12a41..f951ac57fb0d 100644 --- a/src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs +++ b/src/Compilers/CSharp/Portable/CodeGen/EmitExpression.cs @@ -1903,11 +1903,15 @@ CallKind determineEmitReceiverStrategy(BoundCall call, out AddressKind? addressK { // calling a method defined in a base class or interface. - // When calling a method that is virtual in metadata on a struct receiver, + // When calling a method that is virtual in metadata on a struct receiver, // we use a constrained virtual call. If possible, it will skip boxing. if (method.IsMetadataVirtual()) { - addressKind = AddressKind.Writeable; + // For readonly value type receivers, we only need readonly access since + // readonly structs guarantee non-mutation for all their methods, and the + // constrained call either resolves to a non-mutating method or boxes the + // value (which copies it). Either way, the original receiver is not mutated. + addressKind = receiverType.IsReadOnly ? AddressKind.ReadOnly : AddressKind.Writeable; callKind = CallKind.ConstrainedCallVirt; } else @@ -2192,8 +2196,9 @@ static bool isSafeToDereferenceReceiverRefAfterEvaluatingArgument(BoundExpressio { BoundConversion conv = (BoundConversion)current; Debug.Assert(!conv.ConversionKind.IsUserDefinedConversion()); + Debug.Assert(!conv.ConversionKind.IsUnionConversion()); - if (conv.ConversionKind.IsUserDefinedConversion()) + if (conv.ConversionKind.IsUserDefinedConversion() || conv.ConversionKind.IsUnionConversion()) { return false; } diff --git a/src/Compilers/CSharp/Portable/CodeGen/Optimizer.cs b/src/Compilers/CSharp/Portable/CodeGen/Optimizer.cs index 425e1dff10d9..4d00e96ba71e 100644 --- a/src/Compilers/CSharp/Portable/CodeGen/Optimizer.cs +++ b/src/Compilers/CSharp/Portable/CodeGen/Optimizer.cs @@ -1003,7 +1003,7 @@ public override BoundNode VisitAssignmentOperator(BoundAssignmentOperator node) // but when a pointer is converted to a user-defined ref local, it becomes a use of a "safe" feature where we should guarantee the ref is tracked by GC. else if (localSymbol.RefKind != RefKind.None && localSymbol.SynthesizedKind == SynthesizedLocalKind.UserDefined && - right.Kind == BoundKind.PointerIndirectionOperator) + PointerIndirectionMayFlowToRefResultVisitor.Check(right, _recursionDepth)) { ShouldNotSchedule(localSymbol); } @@ -2027,6 +2027,53 @@ private void DeclareLocal(LocalSymbol local, int stack) } } } + + /// + /// Checks whether there is a pointer indirection that may directly contribute to the final by-ref result of the visited expression. + /// For example, *ptr or ptr->Field, but not Method(ref *ptr). + /// If such expression is assigned to a ref local, we cannot optimize that local away, so that GC can retrack the address. + /// This is a conservative check (we prefer correctness over optimization). + /// + private sealed class PointerIndirectionMayFlowToRefResultVisitor : BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator + { + private bool _pointerIndirectionMayFlowToRefResult; + + private PointerIndirectionMayFlowToRefResultVisitor(int recursionDepth) : base(recursionDepth) { } + + public static bool Check(BoundExpression expression, int recursionDepth) + { + var visitor = new PointerIndirectionMayFlowToRefResultVisitor(recursionDepth); + visitor.Visit(expression); + return visitor._pointerIndirectionMayFlowToRefResult; + } + + public override BoundNode Visit(BoundNode node) + { + if (_pointerIndirectionMayFlowToRefResult) + { + // No need to continue visiting nodes if the result is `true`. + return node; + } + + return base.Visit(node); + } + + public override BoundNode VisitPointerIndirectionOperator(BoundPointerIndirectionOperator node) + { + _pointerIndirectionMayFlowToRefResult = true; + return node; + } + + public override BoundNode VisitCall(BoundCall node) + { + return node; + } + + public override BoundNode VisitFunctionPointerInvocation(BoundFunctionPointerInvocation node) + { + return node; + } + } } // Rewrites the tree to account for destructive nature of stack local reads. diff --git a/src/Compilers/CSharp/Portable/CommandLine/CSharpCommandLineParser.cs b/src/Compilers/CSharp/Portable/CommandLine/CSharpCommandLineParser.cs index c1ab05935943..732849094f4b 100644 --- a/src/Compilers/CSharp/Portable/CommandLine/CSharpCommandLineParser.cs +++ b/src/Compilers/CSharp/Portable/CommandLine/CSharpCommandLineParser.cs @@ -398,8 +398,7 @@ internal sealed override CommandLineArguments CommonParse(IEnumerable ar continue; } - var newChecksumAlgorithm = TryParseHashAlgorithmName(value!); - if (newChecksumAlgorithm == SourceHashAlgorithm.None) + if (!SourceHashAlgorithms.TryParseAlgorithmName(value!, out var newChecksumAlgorithm)) { AddDiagnostic(diagnostics, ErrorCode.FTL_BadChecksumAlgorithm, value); continue; @@ -1569,7 +1568,7 @@ internal sealed override CommandLineArguments CommonParse(IEnumerable ar if (nullableContextOptions != NullableContextOptions.Disable && parseOptions.LanguageVersion < MessageID.IDS_FeatureNullableReferenceTypes.RequiredVersion()) { - diagnostics.Add(new CSDiagnostic(new CSDiagnosticInfo(ErrorCode.ERR_NullableOptionNotAvailable, + diagnostics.Add(new CSDiagnostic(new CSDiagnosticInfo(ErrorCode.ERR_CompilationOptionNotAvailable, "nullable", nullableContextOptions, parseOptions.LanguageVersion.ToDisplayString(), new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureNullableReferenceTypes.RequiredVersion())), Location.None)); } diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs index 2a0220ea567d..9046c9220678 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpCompilation.cs @@ -161,6 +161,11 @@ internal Conversions Conversions /// Lazily caches SyntaxTrees by their xxHash128 checksum. Used to look up the syntax tree referenced by an interceptor. private ImmutableSegmentedDictionary, OneOrMany> _contentHashToSyntaxTree; + /// + /// Lazily caches diagnostics for method body compilations for a given SyntaxTree and TextSpan + /// + private ImmutableArray _methodBodiesInTreeDiagnostics = ImmutableArray.Empty; + internal ExtendedErrorTypeSymbol ImplicitlyTypedVariableUsedInForbiddenZoneType { get @@ -350,29 +355,44 @@ internal bool IsRuntimeAsyncEnabledIn(Symbol? symbol) return false; } - if (symbol is not MethodSymbol method) + if (symbol is not MethodSymbol { IsAsync: true } method) { return false; } Debug.Assert(ReferenceEquals(method.ContainingAssembly, Assembly)); + Debug.Assert(method.IsDefinition); + Debug.Assert(method is not Symbols.Metadata.PE.PEMethodSymbol); - var methodReturn = method.ReturnType.OriginalDefinition; - if (((InternalSpecialType)methodReturn.ExtendedSpecialType) is not ( - InternalSpecialType.System_Threading_Tasks_Task or - InternalSpecialType.System_Threading_Tasks_Task_T or - InternalSpecialType.System_Threading_Tasks_ValueTask or - InternalSpecialType.System_Threading_Tasks_ValueTask_T)) + var runtimeAsyncEnabledInMethod = method.RuntimeAsyncMethodGenerationAttributeSetting switch + { + ThreeState.True => true, + ThreeState.False => false, + _ => Feature(CodeAnalysis.Feature.RuntimeAsync) == "on" + }; + + if (!runtimeAsyncEnabledInMethod) { return false; } - return symbol switch - { - SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.True } => true, - SourceMethodSymbol { IsRuntimeAsyncEnabledInMethod: ThreeState.False } => false, - _ => Feature(CodeAnalysis.Feature.RuntimeAsync) == "on" - }; + var methodReturn = method.ReturnType.OriginalDefinition; + if ((object)methodReturn == LambdaSymbol.ReturnTypeIsBeingInferred) + { + // During lambda return type inference we have not yet established whether + // the return type is Task/ValueTask, so we assume runtime async to allow + // caching to be used for the majority case when the return type is indeed + // Task/ValueTask-based. If the return type ends up not being Task/ValueTask, + // that will bust the cache and ensure the body is re-bound with the correct + // handling + return true; + } + + return ((InternalSpecialType)methodReturn.ExtendedSpecialType) is ( + InternalSpecialType.System_Threading_Tasks_Task or + InternalSpecialType.System_Threading_Tasks_Task_T or + InternalSpecialType.System_Threading_Tasks_ValueTask or + InternalSpecialType.System_Threading_Tasks_ValueTask_T); } /// @@ -3074,11 +3094,18 @@ private void GetDiagnosticsWithoutSeverityFiltering(CompilationStage stage, bool if (Options.NullableContextOptions != NullableContextOptions.Disable && LanguageVersion < MessageID.IDS_FeatureNullableReferenceTypes.RequiredVersion() && _syntaxAndDeclarations.ExternalSyntaxTrees.Any()) { - builder.Add(new CSDiagnostic(new CSDiagnosticInfo(ErrorCode.ERR_NullableOptionNotAvailable, + builder.Add(new CSDiagnostic(new CSDiagnosticInfo(ErrorCode.ERR_CompilationOptionNotAvailable, nameof(Options.NullableContextOptions), Options.NullableContextOptions, LanguageVersion.ToDisplayString(), new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureNullableReferenceTypes.RequiredVersion())), Location.None)); } + if (Options.UseUpdatedMemorySafetyRules && !this.IsFeatureEnabled(MessageID.IDS_FeatureUnsafeEvolution)) + { + builder.Add(new CSDiagnostic(new CSDiagnosticInfo(ErrorCode.ERR_CompilationOptionNotAvailable, + nameof(Options.MemorySafetyRules), Options.MemorySafetyRules, LanguageVersion.ToDisplayString(), + new CSharpRequiredLanguageVersion(MessageID.IDS_FeatureUnsafeEvolution.RequiredVersion())), Location.None)); + } + cancellationToken.ThrowIfCancellationRequested(); // the set of diagnostics related to establishing references. @@ -3167,8 +3194,35 @@ private static bool IsDefinedOrImplementedInSourceTree(Symbol symbol, SyntaxTree return false; } + private struct MethodBodyDiagnostics + { + public SyntaxTree Tree { get; } + public TextSpan? Span { get; } + public ImmutableArray Diagnostics { get; } + + public MethodBodyDiagnostics(SyntaxTree tree, TextSpan? span, ImmutableArray diagnostics) + { + Tree = tree; + Span = span; + Diagnostics = diagnostics; + } + } + private ImmutableArray GetDiagnosticsForMethodBodiesInTree(SyntaxTree tree, TextSpan? span, CancellationToken cancellationToken) { + const int MaxCachedMethodBodiesInTreeDiagnostics = 10; + + Debug.Assert(this.ContainsSyntaxTree(tree)); + + var cachedDiagnostics = _methodBodiesInTreeDiagnostics; + foreach (var methodBodyDiagnostics in cachedDiagnostics) + { + if (methodBodyDiagnostics.Tree == tree && methodBodyDiagnostics.Span == span) + { + return methodBodyDiagnostics.Diagnostics; + } + } + var bindingDiagnostics = BindingDiagnosticBag.GetInstance(withDiagnostics: true, withDependencies: false); Debug.Assert(bindingDiagnostics.DiagnosticBag is { }); @@ -3246,7 +3300,46 @@ private ImmutableArray GetDiagnosticsForMethodBodiesInTree(SyntaxTre ReportUnusedImports(tree, bindingDiagnostics, cancellationToken); } - return bindingDiagnostics.ToReadOnlyAndFree().Diagnostics; + var diagnostics = bindingDiagnostics.ToReadOnlyAndFree().Diagnostics; + updateCachedDiagnostics(diagnostics, tree, span); + + return diagnostics; + + void updateCachedDiagnostics(ImmutableArray diagnostics, SyntaxTree tree, TextSpan? span) + { + bool needsUpdate = true; + while (needsUpdate) + { + var cachedDiagnostics = _methodBodiesInTreeDiagnostics; + foreach (var methodBodyDiagnostics in cachedDiagnostics) + { + if (methodBodyDiagnostics.Tree == tree && methodBodyDiagnostics.Span == span) + { + // Someone else already computed diagnostics for this tree/span and updated the cache while we were doing our work. + Debug.Assert(methodBodyDiagnostics.Diagnostics.SequenceEqual(diagnostics)); + return; + } + } + + var newDiagnostics = cachedDiagnostics; + if (newDiagnostics.Length >= MaxCachedMethodBodiesInTreeDiagnostics) + { + // Cache is full, evict the first half. Local testing usually indicates very few entries in the cache, + // so this should be sufficient to keep the cache effective while avoiding unbounded memory growth. + var halfSize = MaxCachedMethodBodiesInTreeDiagnostics / 2; + newDiagnostics = newDiagnostics.RemoveRange(0, halfSize); + } + + newDiagnostics = newDiagnostics.Add(new MethodBodyDiagnostics(tree, span, diagnostics)); + + // Only update the cache if it hasn't changed since we read it, otherwise we might lose diagnostics from another thread that is doing the same thing. + var originalDiagnostics = ImmutableInterlocked.InterlockedCompareExchange(ref _methodBodiesInTreeDiagnostics, newDiagnostics, cachedDiagnostics); + + // If the original diagnostics are not what we did the compare exchange above, the call won't have updated _methodBodiesInTreeDiagnostics + // and we'll need to do another iteration to make sure our diagnostics are in the cache. + needsUpdate = (originalDiagnostics != cachedDiagnostics); + } + } void compileMethodBodiesAndDocComments(SyntaxTree? filterTree, TextSpan? filterSpan, BindingDiagnosticBag bindingDiagnostics, CancellationToken cancellationToken) { diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpDeterministicKeyBuilder.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpDeterministicKeyBuilder.cs index 67dba337835d..c1fd61e433b1 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpDeterministicKeyBuilder.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpDeterministicKeyBuilder.cs @@ -26,6 +26,7 @@ protected override void WriteCompilationOptionsCore(JsonWriter writer, Compilati base.WriteCompilationOptionsCore(writer, options); writer.Write("unsafe", csharpOptions.AllowUnsafe); + writer.Write("memorySafetyRules", csharpOptions.MemorySafetyRules); writer.Write("topLevelBinderFlags", csharpOptions.TopLevelBinderFlags); writer.WriteKey("usings"); writer.WriteArrayStart(); diff --git a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs index aec3389cb274..26b7e848bb58 100644 --- a/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/CSharpSemanticModel.cs @@ -3549,8 +3549,21 @@ boundNode.ExpressionSymbol is Symbol accessSymbol && { GetSymbolsAndResultKind(conversion, conversion.SymbolOpt, conversion.Conversion.OriginalUserDefinedConversions, out symbols, out resultKind); } + else if (conversion.ConversionKind.IsUnionConversion()) + { + Debug.Assert(conversion.SymbolOpt is { }); + GetSymbolsAndResultKind(conversion, conversion.SymbolOpt, originalCandidates: [], out symbols, out resultKind); + } else { + if (conversion.ConversionGroupOpt?.Conversion.IsUnion == true && + conversion.Operand is BoundConversion { Conversion.IsUnion: true } unionConversion && + unionConversion.ConversionGroupOpt == conversion.ConversionGroupOpt) + { + Debug.Assert(unionConversion.SymbolOpt is { }); + GetSymbolsAndResultKind(unionConversion, unionConversion.SymbolOpt, originalCandidates: [], out symbols, out resultKind); + } + goto default; } } diff --git a/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs index 017ef8b8991b..1f64dc824fd0 100644 --- a/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/MemberSemanticModel.cs @@ -1035,8 +1035,8 @@ public override ForEachStatementInfo GetForEachStatementInfo(CommonForEachStatem public override DeconstructionInfo GetDeconstructionInfo(AssignmentExpressionSyntax node) { - var boundDeconstruction = GetUpperBoundNode(node) as BoundDeconstructionAssignmentOperator; - if (boundDeconstruction is null) + var lowerNode = GetLowerBoundNode(node); + if (lowerNode is not BoundDeconstructionAssignmentOperator boundDeconstruction) { return default; } diff --git a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs index 4eda8fbb98eb..e5b52066a60d 100644 --- a/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs +++ b/src/Compilers/CSharp/Portable/Compilation/SyntaxTreeSemanticModel.cs @@ -1679,6 +1679,7 @@ private string GetDeclarationName(CSharpSyntaxNode declaration) case SyntaxKind.InterfaceDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: case SyntaxKind.ClassDeclaration: case SyntaxKind.EnumDeclaration: case SyntaxKind.RecordDeclaration: diff --git a/src/Compilers/CSharp/Portable/Declarations/DeclarationKind.cs b/src/Compilers/CSharp/Portable/Declarations/DeclarationKind.cs index 739e62c0ef1e..5e4978649952 100644 --- a/src/Compilers/CSharp/Portable/Declarations/DeclarationKind.cs +++ b/src/Compilers/CSharp/Portable/Declarations/DeclarationKind.cs @@ -25,6 +25,7 @@ internal enum DeclarationKind : byte Record, RecordStruct, Extension, + Union, } internal static partial class EnumConversions @@ -36,6 +37,7 @@ internal static DeclarationKind ToDeclarationKind(this SyntaxKind kind) case SyntaxKind.ClassDeclaration: return DeclarationKind.Class; case SyntaxKind.InterfaceDeclaration: return DeclarationKind.Interface; case SyntaxKind.StructDeclaration: return DeclarationKind.Struct; + case SyntaxKind.UnionDeclaration: return DeclarationKind.Union; case SyntaxKind.NamespaceDeclaration: case SyntaxKind.FileScopedNamespaceDeclaration: return DeclarationKind.Namespace; diff --git a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs index 04187d67b0ed..995b4ea69482 100644 --- a/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs +++ b/src/Compilers/CSharp/Portable/Declarations/DeclarationTreeBuilder.cs @@ -109,6 +109,7 @@ public static bool CachesComputedMemberNames(SingleTypeDeclaration typeDeclarati DeclarationKind.Class or DeclarationKind.Interface or DeclarationKind.Struct or + DeclarationKind.Union or DeclarationKind.Enum or DeclarationKind.Script or DeclarationKind.Submission or @@ -661,7 +662,14 @@ public override SingleNamespaceOrTypeDeclaration VisitClassDeclaration(ClassDecl public override SingleNamespaceOrTypeDeclaration VisitStructDeclaration(StructDeclarationSyntax node) { - return VisitTypeDeclaration(node, DeclarationKind.Struct); + var declarationKind = node.Kind() switch + { + SyntaxKind.StructDeclaration => DeclarationKind.Struct, + SyntaxKind.UnionDeclaration => DeclarationKind.Union, + _ => throw ExceptionUtilities.UnexpectedValue(node.Kind()) + }; + + return VisitTypeDeclaration(node, declarationKind); } public override SingleNamespaceOrTypeDeclaration VisitInterfaceDeclaration(InterfaceDeclarationSyntax node) @@ -703,7 +711,12 @@ private SingleTypeDeclaration VisitTypeDeclaration(TypeDeclarationSyntax node, D Symbol.ReportErrorIfHasConstraints(node.ConstraintClauses, diagnostics); } - var hasPrimaryCtor = node.ParameterList != null && node is RecordDeclarationSyntax or ClassDeclarationSyntax or StructDeclarationSyntax; + var hasPrimaryCtor = + node.ParameterList != null && + node is RecordDeclarationSyntax or + ClassDeclarationSyntax or + StructDeclarationSyntax { RawKind: not (int)SyntaxKind.UnionDeclaration }; + if (hasPrimaryCtor) { declFlags |= SingleTypeDeclaration.TypeDeclarationFlags.HasAnyNontypeMembers; @@ -718,6 +731,10 @@ private SingleTypeDeclaration VisitTypeDeclaration(TypeDeclarationSyntax node, D } } } + else if (node is StructDeclarationSyntax { RawKind: (int)SyntaxKind.UnionDeclaration }) + { + declFlags |= SingleTypeDeclaration.TypeDeclarationFlags.HasAnyNontypeMembers; // https://github.com/dotnet/roslyn/issues/82636: Add test coverage + } var memberNames = GetNonTypeMemberNames( node, ((Syntax.InternalSyntax.TypeDeclarationSyntax)(node.Green)).Members, @@ -752,6 +769,10 @@ private SingleTypeDeclaration VisitTypeDeclaration(TypeDeclarationSyntax node, D MessageID.IDS_FeaturePrimaryConstructors.CheckFeatureAvailability(diagnostics, node, node.SemicolonToken.GetLocation()); } } + else if (node.Kind() is SyntaxKind.UnionDeclaration) + { + MessageID.IDS_FeatureUnions.CheckFeatureAvailability(diagnostics, node, node.Keyword.GetLocation()); // https://github.com/dotnet/roslyn/issues/82636: Add test coverage, manual tree creation is needed + } var modifiers = node.Modifiers.ToDeclarationModifiers(isForTypeDeclaration: true, diagnostics: diagnostics); var quickAttributes = GetQuickAttributes(node.AttributeLists); @@ -1103,6 +1124,7 @@ private static bool CheckMemberForAttributes(Syntax.InternalSyntax.CSharpSyntaxN case SyntaxKind.ClassDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: // https://github.com/dotnet/roslyn/issues/82636: Add test coverage case SyntaxKind.InterfaceDeclaration: case SyntaxKind.EnumDeclaration: case SyntaxKind.RecordDeclaration: diff --git a/src/Compilers/CSharp/Portable/Declarations/MergedNamespaceDeclaration.cs b/src/Compilers/CSharp/Portable/Declarations/MergedNamespaceDeclaration.cs index 394ef7cbe16c..95038b2f9b26 100644 --- a/src/Compilers/CSharp/Portable/Declarations/MergedNamespaceDeclaration.cs +++ b/src/Compilers/CSharp/Portable/Declarations/MergedNamespaceDeclaration.cs @@ -6,9 +6,10 @@ using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.InteropServices; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.PooledObjects; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp { @@ -155,25 +156,91 @@ static void addNamespacesToChildren(ArrayBuilder nam } else { - // PERF: Don't use ArrayBuilder.ToDictionary directly as it requires an extra dictionary allocation. Other options such - // as MultiDictionary and Dictionary> - // are even less appealing as they don't perform well when their value sets grow to contain a large number of items, - // as typically happens when processing the namespaces. - var namespaceGroups = new Dictionary>(StringOrdinalComparer.Instance); + // PERF: Not using ArrayBuilder as the value in this dictionary as these arrays commonly + // exceed the builder threshold. Instead, calculate the number of SingleNamespaceDeclaration + // for each name and create an exactly sized array. + var namespaceGroups = PooledDictionary.GetInstance(); + var namespaceCounts = PooledDictionary.GetInstance(); - foreach (var n in namespaces) + // First pass - collect the number of times each namespace name is present + populateNamespaceCounts(namespaces, namespaceCounts); + + // Second pass - populate the mapping from namespace name to matching namespace declarations + populateNamespaceGroups(namespaces, namespaceGroups, namespaceCounts); + + // Third pass - populate the children collection based on the namespace groupings + populateChildren(children, namespaceGroups); + + namespaces.Free(); + namespaceCounts.Free(); + namespaceGroups.Free(); + } + } + + static void populateNamespaceCounts(ArrayBuilder namespaces, PooledDictionary namespaceCounts) + { + Debug.Assert(namespaces.Count > 0); + + var name = namespaces[0].Name; + var count = 0; + + foreach (var n in namespaces) + { + // Slight optimization as same named namespaces are likely grouped together. This is a high-traffic codepath + // and this reduces dictionary lookups + if (n.Name != name) { - var builder = namespaceGroups.GetOrAdd(n.Name, static () => ArrayBuilder.GetInstance()); + // Write out to the dictionary the updated count for name + namespaceCounts[name] = count; - builder.Add(n); + name = n.Name; + count = namespaceCounts.TryGetValue(name, out var oldCount) ? oldCount : 0; } - namespaces.Free(); + count++; + } - foreach (var (_, namespaceGroup) in namespaceGroups) + // Write out to the dictionary the updated count for name + namespaceCounts[name] = count; + } + + static void populateNamespaceGroups(ArrayBuilder namespaces, PooledDictionary namespaceGroups, PooledDictionary namespaceCounts) + { + Debug.Assert(namespaces.Count > 0); + + var name = namespaces[0].Name; + var declarations = new SingleNamespaceDeclaration[namespaceCounts[name]]; + var index = 0; + + foreach (var n in namespaces) + { + // Slight optimization as same named namespaces are likely grouped together. This is a high-traffic codepath + // and this reduces dictionary lookups and writes + if (n.Name != name) { - children.Add(MergedNamespaceDeclaration.Create(namespaceGroup.ToImmutableAndFree())); + // Write out to the dictionary the updated declarations and index for name + namespaceGroups[name] = (declarations, index); + + name = n.Name; + (declarations, index) = namespaceGroups.TryGetValue(name, out var declAndIndex) + ? declAndIndex + : (new SingleNamespaceDeclaration[namespaceCounts[name]], 0); } + + declarations[index] = n; + index++; + } + + // Write out to the dictionary the updated declarations and index for name + namespaceGroups[name] = (declarations, index); + } + + static void populateChildren(ArrayBuilder children, PooledDictionary namespaceGroups) + { + foreach (var (_, namespaceGroup) in namespaceGroups) + { + var declarations = ImmutableCollectionsMarshal.AsImmutableArray(namespaceGroup.Declarations); + children.Add(MergedNamespaceDeclaration.Create(declarations)); } } } diff --git a/src/Compilers/CSharp/Portable/Declarations/MergedTypeDeclaration.cs b/src/Compilers/CSharp/Portable/Declarations/MergedTypeDeclaration.cs index c7849169701f..1c45b91aeac1 100644 --- a/src/Compilers/CSharp/Portable/Declarations/MergedTypeDeclaration.cs +++ b/src/Compilers/CSharp/Portable/Declarations/MergedTypeDeclaration.cs @@ -76,6 +76,7 @@ public ImmutableArray> GetAttributeDeclarations( { case SyntaxKind.ClassDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.RecordStructDeclaration: diff --git a/src/Compilers/CSharp/Portable/DocumentationComments/DocumentationCommentIDVisitor.PartVisitor.cs b/src/Compilers/CSharp/Portable/DocumentationComments/DocumentationCommentIDVisitor.PartVisitor.cs index 1c0ff2978a69..96f8b649cf59 100644 --- a/src/Compilers/CSharp/Portable/DocumentationComments/DocumentationCommentIDVisitor.PartVisitor.cs +++ b/src/Compilers/CSharp/Portable/DocumentationComments/DocumentationCommentIDVisitor.PartVisitor.cs @@ -4,17 +4,14 @@ #nullable disable +using System; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Text; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.CSharp.Symbols; -using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; -using System; namespace Microsoft.CodeAnalysis.CSharp { @@ -32,6 +29,8 @@ private sealed class PartVisitor : CSharpSymbolVisitor // Select callers within this type use this one. private static readonly PartVisitor s_parameterOrReturnTypeInstance = new PartVisitor(inParameterOrReturnType: true); + private static readonly char[] s_escapedMetadataNameChars = [':', '.', '<', '>']; + private readonly bool _inParameterOrReturnType; private PartVisitor(bool inParameterOrReturnType) @@ -274,6 +273,11 @@ private static string GetEscapedMetadataName(Symbol symbol) { string metadataName = symbol.MetadataName; + if (metadataName.IndexOfAny(s_escapedMetadataNameChars) == -1) + { + return metadataName; + } + int colonColonIndex = metadataName.IndexOf("::", StringComparison.Ordinal); int startIndex = colonColonIndex < 0 ? 0 : colonColonIndex + 2; diff --git a/src/Compilers/CSharp/Portable/Emitter/Model/PEAssemblyBuilder.cs b/src/Compilers/CSharp/Portable/Emitter/Model/PEAssemblyBuilder.cs index 7caddab400db..a46ffcc25527 100644 --- a/src/Compilers/CSharp/Portable/Emitter/Model/PEAssemblyBuilder.cs +++ b/src/Compilers/CSharp/Portable/Emitter/Model/PEAssemblyBuilder.cs @@ -45,6 +45,7 @@ internal abstract class PEAssemblyBuilderBase : PEModuleBuilder, Cci.IAssemblyRe private SynthesizedEmbeddedNativeIntegerAttributeSymbol _lazyNativeIntegerAttribute; private SynthesizedEmbeddedScopedRefAttributeSymbol _lazyScopedRefAttribute; private SynthesizedEmbeddedRefSafetyRulesAttributeSymbol _lazyRefSafetyRulesAttribute; + private SynthesizedEmbeddedMemorySafetyRulesAttributeSymbol _lazyMemorySafetyRulesAttribute; private SynthesizedEmbeddedExtensionMarkerAttributeSymbol _lazyExtensionMarkerAttribute; /// @@ -112,6 +113,7 @@ internal sealed override ImmutableArray GetEmbeddedTypes(Bindin builder.AddIfNotNull(_lazyNativeIntegerAttribute); builder.AddIfNotNull(_lazyScopedRefAttribute); builder.AddIfNotNull(_lazyRefSafetyRulesAttribute); + builder.AddIfNotNull(_lazyMemorySafetyRulesAttribute); builder.AddIfNotNull(_lazyExtensionMarkerAttribute); return builder.ToImmutableAndFree(); @@ -297,6 +299,20 @@ internal override SynthesizedAttributeData SynthesizeRefSafetyRulesAttribute(Imm return base.SynthesizeRefSafetyRulesAttribute(arguments); } + internal override SynthesizedAttributeData TrySynthesizeMemorySafetyRulesAttribute(ImmutableArray arguments) + { + if ((object)_lazyMemorySafetyRulesAttribute != null) + { + return SynthesizedAttributeData.Create( + Compilation, + _lazyMemorySafetyRulesAttribute.Constructors[0], + arguments, + namedArguments: []); + } + + return base.TrySynthesizeMemorySafetyRulesAttribute(arguments); + } + protected override SynthesizedAttributeData TrySynthesizeIsReadOnlyAttribute() { if ((object)_lazyIsReadOnlyAttribute != null) @@ -396,12 +412,20 @@ private void CreateEmbeddedAttributesIfNeeded(BindingDiagnosticBag diagnostics) needsAttributes |= EmbeddableAttributes.NullablePublicOnlyAttribute; } - if (((SourceModuleSymbol)Compilation.SourceModule).RequiresRefSafetyRulesAttribute() && + var sourceModule = (SourceModuleSymbol)Compilation.SourceModule; + + if (sourceModule.RequiresRefSafetyRulesAttribute() && Compilation.CheckIfAttributeShouldBeEmbedded(EmbeddableAttributes.RefSafetyRulesAttribute, diagnostics, Location.None)) { needsAttributes |= EmbeddableAttributes.RefSafetyRulesAttribute; } + if (sourceModule.UseUpdatedMemorySafetyRules && + Compilation.CheckIfAttributeShouldBeEmbedded(EmbeddableAttributes.MemorySafetyRulesAttribute, diagnostics, Location.None)) + { + needsAttributes |= EmbeddableAttributes.MemorySafetyRulesAttribute; + } + if (needsAttributes == 0) { return; @@ -516,6 +540,15 @@ private void CreateEmbeddedAttributesIfNeeded(BindingDiagnosticBag diagnostics) CreateRefSafetyRulesAttributeSymbol); } + if ((needsAttributes & EmbeddableAttributes.MemorySafetyRulesAttribute) != 0) + { + CreateAttributeIfNeeded( + ref _lazyMemorySafetyRulesAttribute, + diagnostics, + AttributeDescription.MemorySafetyRulesAttribute, + CreateMemorySafetyRulesAttributeSymbol); + } + if ((needsAttributes & EmbeddableAttributes.ExtensionMarkerAttribute) != 0) { CreateAttributeIfNeeded( @@ -580,6 +613,14 @@ private SynthesizedEmbeddedRefSafetyRulesAttributeSymbol CreateRefSafetyRulesAtt GetWellKnownType(WellKnownType.System_Attribute, diagnostics), GetSpecialType(SpecialType.System_Int32, diagnostics)); + private SynthesizedEmbeddedMemorySafetyRulesAttributeSymbol CreateMemorySafetyRulesAttributeSymbol(string name, NamespaceSymbol containingNamespace, BindingDiagnosticBag diagnostics) + => new SynthesizedEmbeddedMemorySafetyRulesAttributeSymbol( + name, + containingNamespace, + SourceModule, + systemAttributeType: GetWellKnownType(WellKnownType.System_Attribute, diagnostics), + int32Type: GetSpecialType(SpecialType.System_Int32, diagnostics)); + private SynthesizedEmbeddedExtensionMarkerAttributeSymbol CreateExtensionMarkerAttributeSymbol(string name, NamespaceSymbol containingNamespace, BindingDiagnosticBag diagnostics) => new SynthesizedEmbeddedExtensionMarkerAttributeSymbol( name, diff --git a/src/Compilers/CSharp/Portable/Emitter/Model/PEModuleBuilder.cs b/src/Compilers/CSharp/Portable/Emitter/Model/PEModuleBuilder.cs index c02e5e4d990f..02764c7c5fb6 100644 --- a/src/Compilers/CSharp/Portable/Emitter/Model/PEModuleBuilder.cs +++ b/src/Compilers/CSharp/Portable/Emitter/Model/PEModuleBuilder.cs @@ -1872,6 +1872,12 @@ internal virtual SynthesizedAttributeData SynthesizeRefSafetyRulesAttribute(Immu return Compilation.TrySynthesizeAttribute(WellKnownMember.System_Runtime_CompilerServices_RefSafetyRulesAttribute__ctor, arguments, isOptionalUse: true); } + internal virtual SynthesizedAttributeData TrySynthesizeMemorySafetyRulesAttribute(ImmutableArray arguments) + { + // For modules, this attribute should be present. Only assemblies generate and embed this type. + return Compilation.TrySynthesizeAttribute(WellKnownMember.System_Runtime_CompilerServices_MemorySafetyRulesAttribute__ctor, arguments); + } + internal bool ShouldEmitNullablePublicOnlyAttribute() { // No need to look at this.GetNeedsGeneratedAttributes() since those bits are diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs index 22673e0239d8..6ca908806a0b 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorCode.cs @@ -1676,7 +1676,7 @@ internal enum ErrorCode ERR_NullableUnconstrainedTypeParameter = 8627, ERR_AnnotationDisallowedInObjectCreation = 8628, WRN_NullableValueTypeMayBeNull = 8629, - ERR_NullableOptionNotAvailable = 8630, + ERR_CompilationOptionNotAvailable = 8630, WRN_NullabilityMismatchInTypeParameterConstraint = 8631, WRN_MissingNonNullTypesContextForAnnotation = 8632, WRN_NullabilityMismatchInConstraintsOnImplicitImplementation = 8633, @@ -2161,7 +2161,7 @@ internal enum ErrorCode WRN_ParamsArrayInLambdaOnly = 9100, ERR_UnscopedRefAttributeUnsupportedMemberTarget = 9101, ERR_UnscopedRefAttributeInterfaceImplementation = 9102, - ERR_UnrecognizedRefSafetyRulesAttributeVersion = 9103, + ERR_UnrecognizedAttributeVersion = 9103, // ERR_BadSpecialByRefUsing = 9104, ERR_InvalidPrimaryConstructorParameterReference = 9105, @@ -2463,10 +2463,31 @@ internal enum ErrorCode ERR_CollectionRefLikeElementType = 9358, ERR_BadCollectionArgumentsArgCount = 9359, - ERR_ClosedTypeNameDisallowed = 9365, // PROTOTYPE(cc): pack - ERR_ClosedSealedStatic = 9366, - ERR_ClosedBaseTypeBaseFromOtherAssembly = 9367, - ERR_UnderspecifiedClosedSubtype = 9368, + ERR_UnsafeOperation = 9360, + ERR_UnsafeUninitializedStackAlloc = 9361, + ERR_UnsafeMemberOperation = 9362, + ERR_UnsafeMemberOperationCompat = 9363, + ERR_CallerUnsafeOverridingSafe = 9364, + ERR_CallerUnsafeImplicitlyImplementingSafe = 9365, + ERR_CallerUnsafeExplicitlyImplementingSafe = 9366, + ERR_RequiresUnsafeAttributeUnsupportedMemberTarget = 9367, + WRN_RequiresUnsafeAttributeLegacyRules = 9368, + + ERR_ExpressionTreeContainsUnionConversion = 9369, + ERR_UnionDeclarationNeedsCaseTypes = 9370, + ERR_NoImplicitConversionToObject = 9371, + ERR_UnionMatchingWrongPattern = 9372, + ERR_InstanceFieldInUnion = 9373, + ERR_InstanceCtorWithOneParameterInUnion = 9374, + ERR_UnionConstructorCallsDefaultConstructor = 9375, + + ERR_UnsafeConstructorConstraint = 9376, + WRN_UnsafeMeaningless = 9377, + + ERR_ClosedTypeNameDisallowed = 9600, // PROTOTYPE(cc): pack + ERR_ClosedSealedStatic = 9601, + ERR_ClosedBaseTypeBaseFromOtherAssembly = 9602, + ERR_UnderspecifiedClosedSubtype = 9603, // Note: you will need to do the following after adding errors: // 1) Update ErrorFacts.IsBuildOnlyDiagnostic (src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs) diff --git a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs index 4e37ed0620e8..9454a160bf0a 100644 --- a/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs +++ b/src/Compilers/CSharp/Portable/Errors/ErrorFacts.cs @@ -210,6 +210,11 @@ internal static int GetWarningLevel(ErrorCode code) // docs/compilers/CSharp/Warnversion Warning Waves.md switch (code) { + case ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules: + case ErrorCode.WRN_UnsafeMeaningless: + // Warning level 11 is exclusively for warnings introduced in the compiler + // shipped with dotnet 11 (C# 15) and that can be reported for pre-existing code. + return 11; case ErrorCode.WRN_UnassignedInternalRefField: // Warning level 10 is exclusively for warnings introduced in the compiler // shipped with dotnet 10 (C# 14) and that can be reported for pre-existing code. @@ -1971,7 +1976,7 @@ or ErrorCode.WRN_NullAsNonNullable or ErrorCode.ERR_NullableUnconstrainedTypeParameter or ErrorCode.ERR_AnnotationDisallowedInObjectCreation or ErrorCode.WRN_NullableValueTypeMayBeNull - or ErrorCode.ERR_NullableOptionNotAvailable + or ErrorCode.ERR_CompilationOptionNotAvailable or ErrorCode.WRN_NullabilityMismatchInTypeParameterConstraint or ErrorCode.WRN_MissingNonNullTypesContextForAnnotation or ErrorCode.WRN_NullabilityMismatchInConstraintsOnImplicitImplementation @@ -2344,7 +2349,7 @@ or ErrorCode.WRN_OptionalParamValueMismatch or ErrorCode.WRN_ParamsArrayInLambdaOnly or ErrorCode.ERR_UnscopedRefAttributeUnsupportedMemberTarget or ErrorCode.ERR_UnscopedRefAttributeInterfaceImplementation - or ErrorCode.ERR_UnrecognizedRefSafetyRulesAttributeVersion + or ErrorCode.ERR_UnrecognizedAttributeVersion or ErrorCode.ERR_InvalidPrimaryConstructorParameterReference or ErrorCode.ERR_AmbiguousPrimaryConstructorParameterAsColorColorReceiver or ErrorCode.WRN_CapturedPrimaryConstructorParameterPassedToBase @@ -2566,6 +2571,24 @@ or ErrorCode.ERR_InvalidModifierAfterScoped or ErrorCode.ERR_StructLayoutAndExtendedLayout or ErrorCode.ERR_RuntimeDoesNotSupportExtendedLayoutTypes or ErrorCode.ERR_NoAwaitOnAsyncEnumerable + or ErrorCode.ERR_UnsafeOperation + or ErrorCode.ERR_UnsafeUninitializedStackAlloc + or ErrorCode.ERR_UnsafeMemberOperation + or ErrorCode.ERR_UnsafeMemberOperationCompat + or ErrorCode.ERR_CallerUnsafeOverridingSafe + or ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe + or ErrorCode.ERR_CallerUnsafeExplicitlyImplementingSafe + or ErrorCode.ERR_RequiresUnsafeAttributeUnsupportedMemberTarget + or ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules + or ErrorCode.ERR_ExpressionTreeContainsUnionConversion + or ErrorCode.ERR_UnionDeclarationNeedsCaseTypes + or ErrorCode.ERR_NoImplicitConversionToObject + or ErrorCode.ERR_UnionMatchingWrongPattern + or ErrorCode.ERR_InstanceFieldInUnion + or ErrorCode.ERR_InstanceCtorWithOneParameterInUnion + or ErrorCode.ERR_UnionConstructorCallsDefaultConstructor + or ErrorCode.ERR_UnsafeConstructorConstraint + or ErrorCode.WRN_UnsafeMeaningless or ErrorCode.ERR_ClosedTypeNameDisallowed or ErrorCode.ERR_ClosedSealedStatic or ErrorCode.ERR_ClosedBaseTypeBaseFromOtherAssembly diff --git a/src/Compilers/CSharp/Portable/Errors/MessageID.cs b/src/Compilers/CSharp/Portable/Errors/MessageID.cs index 5310e66fd03e..38940edd73f7 100644 --- a/src/Compilers/CSharp/Portable/Errors/MessageID.cs +++ b/src/Compilers/CSharp/Portable/Errors/MessageID.cs @@ -307,6 +307,11 @@ internal enum MessageID IDS_FeatureUserDefinedCompoundAssignmentOperators = MessageBase + 12857, IDS_FeatureCollectionExpressionArguments = MessageBase + 12858, + + IDS_FeatureUnsafeEvolution = MessageBase + 12859, + + IDS_FeatureUnions = MessageBase + 12860, + IDS_FeatureClosedClasses = MessageBase + 12865, // PROTOTYPE(cc): pack } @@ -489,6 +494,8 @@ internal static LanguageVersion RequiredVersion(this MessageID feature) // C# preview features. case MessageID.IDS_FeatureCollectionExpressionArguments: + case MessageID.IDS_FeatureUnsafeEvolution: // https://github.com/dotnet/roslyn/issues/82546: keep this in preview until C# 16 + case MessageID.IDS_FeatureUnions: case MessageID.IDS_FeatureClosedClasses: // semantic check return LanguageVersion.Preview; diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs index 090c2d73d578..c97aae35658b 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/AbstractFlowPass.cs @@ -374,6 +374,7 @@ protected override BoundNode VisitExpressionOrPatternWithoutStackGuard(BoundNode return base.Visit(node); } + [DebuggerStepThrough] protected override bool ConvertInsufficientExecutionStackExceptionToCancelledByStackGuardException() { return false; // just let the original exception bubble up. @@ -930,6 +931,54 @@ BoundDeclarationPattern or BoundConstantPattern or BoundNegatedPattern or BoundB bool negated = node.Pattern.IsNegated(out var pattern); Debug.Assert(negated == node.IsNegated); + // Note that 'IsNegated' helper used above doesn't unwrap a negation utilizing a Union matchig. + // A comment in 'IsNegated' explains why. + // However, for the purposes of this component we have to do the unwrapping because + // 'DefiniteAssignmentPass.VisitPattern' never considers pattern locals under 'NegatedPattern' + // as definitely assigned. In this particular situation, when we are dealing with a struct + // Union type, they should be considered definitely assigned. Therefore, we perform a semantically + // equivalent rewrite, first by rewriting to a recursive pattern, and then by pulling negation out + // of the sub-pattern. In this situation a pattern '{Value: not (...) }' is equivalent to a pattern + // 'not {Value: (...) }' + if (node.HasUnionMatching && + pattern is BoundNegatedPattern { IsUnionMatching: true } && + UnionMatchingRewriter.Rewrite(compilation, pattern) is BoundRecursivePattern + { + WasCompilerGenerated: true, + DeclaredType: null, + InputType: NamedTypeSymbol { TypeKind: TypeKind.Struct, IsUnionType: true } inputType, + DeconstructMethod: null, + Deconstruction: { IsDefault: true }, + Properties: + [ + { + Pattern: { } nestedPattern, + Member: + { Type.SpecialType: SpecialType.System_Object, Symbol: var possibleUnionValueSymbol } and + ({ Symbol: PropertySymbol { Name: WellKnownMemberNames.ValuePropertyName } } or { Symbol: null, HasErrors: true }) + } propertySubpattern + ], + Variable: null, + VariableAccess: null, + IsUnionMatching: false, + } rewritten && + (possibleUnionValueSymbol is null || Binder.IsUnionTypeValueProperty(inputType, possibleUnionValueSymbol))) + { + Debug.Assert(!inputType.IsNullableType()); + Debug.Assert(!negated); + + negated ^= nestedPattern.IsNegated(out var negatedNestedPattern); + + if (nestedPattern != negatedNestedPattern) + { + pattern = rewritten.Update( + rewritten.DeclaredType, rewritten.DeconstructMethod, rewritten.Deconstruction, + [propertySubpattern.Update(propertySubpattern.Member, propertySubpattern.IsLengthOrCount, negatedNestedPattern)], + rewritten.IsExplicitNotNullTest, rewritten.Variable, rewritten.VariableAccess, rewritten.IsUnionMatching, + rewritten.InputType, rewritten.NarrowedType); + } + } + if (VisitPossibleConditionalAccess(node.Expression, out var stateWhenNotNull)) { Debug.Assert(!IsConditionalState); @@ -3035,7 +3084,7 @@ protected static bool CanPropagateStateWhenNotNull(Conversion conversion) return false; } - if (!conversion.IsUserDefined) + if (!conversion.IsUserDefined) // https://github.com/dotnet/roslyn/issues/82636: Follow up { return true; } diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/DefiniteAssignment.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/DefiniteAssignment.cs index b0700eb9fe47..f59cf00e029d 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/DefiniteAssignment.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/DefiniteAssignment.cs @@ -882,6 +882,7 @@ internal static bool WriteConsideredUse(TypeSymbol type, BoundExpression value) // user-defined conversion. Therefore the IntPtr ConversionKind is included // here. if (boundConversion.ConversionKind.IsUserDefinedConversion() || + boundConversion.ConversionKind.IsUnionConversion() || // https://github.com/dotnet/roslyn/issues/82636: Add coverage boundConversion.ConversionKind == ConversionKind.IntPtr) { return true; diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs index ff1493ee2f99..b77e8e27ce92 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker.cs @@ -269,7 +269,7 @@ internal string GetDebuggerDisplay() /// /// Map from a target-typed expression (such as a target-typed conditional, switch or new) to the delegate /// that completes analysis once the target type is known. - /// The delegate is invoked by . + /// The delegate is invoked by . /// private PooledDictionary> TargetTypedAnalysisCompletion => _targetTypedAnalysisCompletionOpt ??= PooledDictionary>.GetInstance(); @@ -529,6 +529,7 @@ public string GetDebuggerDisplay() // For purpose of nullability analysis, awaits create pending branches, so async usings and foreachs do too public sealed override bool AwaitUsingAndForeachAddsPendingBranch => true; + [DebuggerStepThrough] protected override void EnsureSufficientExecutionStack(int recursionDepth) { if (recursionDepth > StackGuard.MaxUncheckedRecursionDepth && @@ -542,6 +543,7 @@ protected override void EnsureSufficientExecutionStack(int recursionDepth) base.EnsureSufficientExecutionStack(recursionDepth); } + [DebuggerStepThrough] protected override bool ConvertInsufficientExecutionStackExceptionToCancelledByStackGuardException() { return true; @@ -595,7 +597,6 @@ private static void AssertPlaceholderAllowedWithoutRegistration(BoundValuePlaceh case BoundKind.InterpolatedStringHandlerPlaceholder: case BoundKind.InterpolatedStringArgumentPlaceholder: case BoundKind.ObjectOrCollectionValuePlaceholder: - case BoundKind.AwaitableValuePlaceholder: return; case BoundKind.ImplicitIndexerValuePlaceholder: @@ -795,10 +796,11 @@ void checkMemberStateOnConstructorExit(MethodSymbol constructor, Symbol member, return; } - // If 'field' keyword is explicitly used by 'symbol', then use FlowAnalysisAnnotations from the backing field. - // Otherwise, use the FlowAnalysisAnnotations from the user-declared symbol (property or ordinary field). - var usesFieldKeyword = symbol is SourcePropertySymbolBase { UsesFieldKeyword: true }; - var annotations = usesFieldKeyword ? field!.FlowAnalysisAnnotations : symbol.GetFlowAnalysisAnnotations(); + // If we are considering the backing field for this check, and the field keyword is explicitly used by 'symbol', then use FlowAnalysisAnnotations from the backing field. + // Otherwise, use the FlowAnalysisAnnotations from 'symbol'. + var (usesFieldKeyword, annotations) = field != null && symbol is SourcePropertySymbolBase { UsesFieldKeyword: true } + ? (true, field.FlowAnalysisAnnotations) + : (false, symbol.GetFlowAnalysisAnnotations()); if ((annotations & FlowAnalysisAnnotations.AllowNull) != 0) { // We assume that if a member has AllowNull then the user @@ -2027,31 +2029,30 @@ private NullableFlowState GetDefaultState(ref LocalState state, int slot) var variable = _variables[slot]; var symbol = variable.Symbol; - switch (symbol.Kind) + switch (symbol) { - case SymbolKind.Local: + case LocalSymbol local: { - var local = (LocalSymbol)symbol; if (!_variables.TryGetType(local, out TypeWithAnnotations localType)) { localType = local.TypeWithAnnotations; } return localType.ToTypeWithState().State; } - case SymbolKind.Parameter: + case ParameterSymbol parameter: { - var parameter = (ParameterSymbol)symbol; if (!_variables.TryGetType(parameter, out TypeWithAnnotations parameterType)) { parameterType = parameter.TypeWithAnnotations; } return GetParameterState(parameterType, parameter.FlowAnalysisAnnotations).State; } - case SymbolKind.Field: - case SymbolKind.Property: - case SymbolKind.Event: + + case FieldSymbol: + case PropertySymbol: + case EventSymbol: return GetDefaultState(symbol); - case SymbolKind.ErrorType: + case { Kind: SymbolKind.ErrorType }: return NullableFlowState.NotNull; default: throw ExceptionUtilities.UnexpectedValue(symbol.Kind); @@ -2736,7 +2737,8 @@ private bool IsSlotMember(int slot, Symbol possibleMember) var conversionsWithoutNullability = _conversions.WithNullability(false); return conversionsWithoutNullability.HasIdentityOrImplicitReferenceConversion(possibleDerived, possibleBase, ref discardedUseSiteInfo) || - conversionsWithoutNullability.HasBoxingConversion(possibleDerived, possibleBase, ref discardedUseSiteInfo); + conversionsWithoutNullability.HasBoxingConversion(possibleDerived, possibleBase, ref discardedUseSiteInfo) || + (possibleBase.IsInterfaceType() && conversionsWithoutNullability.HasImplicitConversionToOrImplementsVarianceCompatibleInterface(possibleDerived, (NamedTypeSymbol)possibleBase, ref discardedUseSiteInfo, needSupportForRefStructInterfaces: out _)); } // 'skipSlot' is the original target slot that should be skipped in case of cycles. @@ -3597,16 +3599,43 @@ private void VisitLocalFunctionUse(LocalFunctionSymbol symbol) public override BoundNode? VisitUsingStatement(BoundUsingStatement node) { DeclareLocals(node.Locals); - Visit(node.AwaitOpt); + VisitAwaitableInfoForUsing(node.AwaitOpt, node.PatternDisposeInfoOpt?.Method); return base.VisitUsingStatement(node); } public override BoundNode? VisitUsingLocalDeclarations(BoundUsingLocalDeclarations node) { - Visit(node.AwaitOpt); + VisitAwaitableInfoForUsing(node.AwaitOpt, node.PatternDisposeInfoOpt?.Method); return base.VisitUsingLocalDeclarations(node); } + private void VisitAwaitableInfoForUsing(BoundAwaitableInfo? awaitInfo, MethodSymbol? patternDisposeMethod) + { + // Placeholder can be null in error scenarios. In such cases, nothing is filled in and errors would + // have been reported already. We can just return. + if (awaitInfo is not { AwaitableInstancePlaceholder: { } placeholder }) + { + return; + } + + VisitResult placeholderResult; + if (patternDisposeMethod is not null) + { + placeholderResult = new VisitResult(GetReturnTypeWithState(patternDisposeMethod), patternDisposeMethod.ReturnTypeWithAnnotations); + } + else + { + // IAsyncDisposable.DisposeAsync returns ValueTask, which is a struct; we know it can never be `ValueTask?`, as that is a different + // type that would not match the descriptor. Any other scenario either has an error already, or will report an + // error during emit when we fail to find IAsyncDisposable.DisposeAsync. + placeholderResult = new VisitResult(placeholder.Type, NullableAnnotation.NotAnnotated, NullableFlowState.NotNull); + } + + AddPlaceholderReplacement(placeholder, placeholder, placeholderResult); + Visit(awaitInfo); + RemovePlaceholderReplacement(placeholder); + } + public override BoundNode? VisitFixedStatement(BoundFixedStatement node) { DeclareLocals(node.Locals); @@ -4331,7 +4360,7 @@ void setAnalyzedNullabilityAsContinuation( int slot = -1; var resultState = NullableFlowState.NotNull; if (type is object && - (hasObjectInitializer || type.IsStructType())) + (hasObjectInitializer || type.IsStructType() || isSuitableUnionConstruction(type, constructor, out _))) { slot = GetOrCreatePlaceholderSlot(node); if (slot > 0) @@ -4380,10 +4409,31 @@ void setAnalyzedNullabilityAsContinuation( } SetState(ref this.State, slot, resultState); + + if (isSuitableUnionConstruction(type, constructor, out PropertySymbol? valueProperty)) + { + // When a union constructor is called, the new union's Value gets the null state of the incoming value. + SetUnionValueStateFromConstructorArgument(arguments[0], argumentResults[0].RValueType, slot, valueProperty); + } } } return (slot, resultState, null); + + static bool isSuitableUnionConstruction(TypeSymbol type, [NotNullWhen(true)] MethodSymbol? constructor, [NotNullWhen(true)] out PropertySymbol? valueProperty) + { + if (constructor is not null && + constructor.ContainingType.Equals(type, TypeCompareKind.AllIgnoreOptions) && + type is NamedTypeSymbol { IsUnionType: true } unionType && + NamedTypeSymbol.IsSuitableUnionConstructor(constructor)) + { + valueProperty = Binder.GetUnionTypeValuePropertyNoUseSiteDiagnostics(unionType); + return valueProperty is { }; + } + + valueProperty = null; + return false; + } } Func inferInitialObjectStateAsContinuation( @@ -4402,6 +4452,29 @@ void setAnalyzedNullabilityAsContinuation( } } + private void SetUnionValueStateFromConstructorArgument(BoundExpression argument, TypeWithState argumentTypeWithState, int containingSlot, PropertySymbol valueProperty) + { + Debug.Assert(containingSlot > 0); + NullableFlowState operandState; + + int operandSlot = MakeSlot(argument); + if (operandSlot > 0) + { + operandState = GetState(ref this.State, operandSlot); + } + else + { + operandState = argumentTypeWithState.State; + } + + int iUnionValuePropertySlot = GetOrCreateSlot(valueProperty, containingSlot); + Debug.Assert(iUnionValuePropertySlot > 0); + if (iUnionValuePropertySlot > 0) + { + SetState(ref this.State, iUnionValuePropertySlot, operandState); + } + } + /// /// If , is known only within returned delegate. /// @@ -6128,18 +6201,20 @@ private static BoundExpression SkipReferenceConversions(BoundExpression possibly GenerateConversionForConditionalOperator(node.LeftOperand, leftType, rightType, reportMismatch: false, isChecked: node.Checked) is { Exists: true } conversion) { Debug.Assert(!conversion.IsUserDefined); + Debug.Assert(!conversion.IsUnion); return (rightType, NullableFlowState.NotNull); } conversion = GenerateConversionForConditionalOperator(node.RightOperand, rightType, leftType, reportMismatch: true, isChecked: node.Checked); Debug.Assert(!conversion.IsUserDefined); + Debug.Assert(!conversion.IsUnion); return (leftType, NullableFlowState.NotNull); } (TypeSymbol ResultType, NullableFlowState LeftState) getResultStateWithRightType(TypeSymbol leftType, TypeSymbol rightType) { var conversion = GenerateConversionForConditionalOperator(node.LeftOperand, leftType, rightType, reportMismatch: true, isChecked: node.Checked); - if (conversion.IsUserDefined) + if (conversion.IsUserDefined) // https://github.com/dotnet/roslyn/issues/82636: Confirm no special handling necessary or add it. { var conversionResult = VisitConversion( conversionOpt: null, @@ -7862,12 +7937,30 @@ private void ApplyMemberPostConditions(int receiverSlot, MethodSymbol method) applyMemberPostConditions(receiverSlot, type, notNullMembers, ref State); } - if (method.ReturnType.SpecialType == SpecialType.System_Boolean - && !(notNullWhenTrueMembers.IsEmpty && notNullWhenFalseMembers.IsEmpty)) + if (method.ReturnType.SpecialType == SpecialType.System_Boolean) { - Split(); - applyMemberPostConditions(receiverSlot, type, notNullWhenTrueMembers, ref StateWhenTrue); - applyMemberPostConditions(receiverSlot, type, notNullWhenFalseMembers, ref StateWhenFalse); + if (!(notNullWhenTrueMembers.IsEmpty && notNullWhenFalseMembers.IsEmpty)) + { + Split(); + applyMemberPostConditions(receiverSlot, type, notNullWhenTrueMembers, ref StateWhenTrue); + applyMemberPostConditions(receiverSlot, type, notNullWhenFalseMembers, ref StateWhenFalse); + } + + if (method is MethodSymbol + { + Name: WellKnownMemberNames.TryGetValueMethodName, + ReturnType.SpecialType: SpecialType.System_Boolean, + DeclaredAccessibility: Accessibility.Public, + RefKind: RefKind.None, + Parameters: [{ RefKind: RefKind.Out, Type: var parameterType }], + ContainingType: { IsUnionType: true } unionType + } tryGetValue && + (object)tryGetValue.OriginalDefinition == Binder.GetUnionTypeTryGetValueMethod(unionType, parameterType)?.OriginalDefinition && // Looking for TryGetValue with exact type match at this call site + Binder.GetUnionTypeValuePropertyNoUseSiteDiagnostics(unionType) is { } unionValue) + { + Split(); + markMemberAsNotNull(receiverSlot, ref StateWhenTrue, unionValue); + } } method = method.OverriddenMethod; @@ -7906,11 +7999,7 @@ void markMembersAsNotNull(int receiverSlot, TypeSymbol type, string memberName, { case SymbolKind.Field: case SymbolKind.Property: - if (GetOrCreateSlot(member, receiverSlot) is int memberSlot && - memberSlot > 0) - { - SetState(ref state, memberSlot, NullableFlowState.NotNull); - } + state = markMemberAsNotNull(receiverSlot, ref state, member); break; case SymbolKind.Event: case SymbolKind.Method: @@ -7918,6 +8007,17 @@ void markMembersAsNotNull(int receiverSlot, TypeSymbol type, string memberName, } } } + + LocalState markMemberAsNotNull(int receiverSlot, ref LocalState state, Symbol member) + { + if (GetOrCreateSlot(member, receiverSlot) is int memberSlot && + memberSlot > 0) + { + SetState(ref state, memberSlot, NullableFlowState.NotNull); + } + + return state; + } } private ImmutableArray VisitArgumentsEvaluate( @@ -8069,7 +8169,7 @@ private void VisitArgumentConversionAndInboundAssignmentsAndPreConditions( case RefKind.In: { // Note: for lambda arguments, they will be converted in the context/state we saved for that argument - if (conversion is { IsValid: true, Kind: ConversionKind.ImplicitUserDefined }) + if (conversion is { IsValid: true, Kind: ConversionKind.ImplicitUserDefined }) // https://github.com/dotnet/roslyn/issues/82636: Do we need to add special handling for Union conversions? { var argumentResultType = resultType.Type; conversion = GenerateConversion(_conversions, argumentNoConversion, argumentResultType, parameterType.Type, fromExplicitCast: false, extensionMethodThisArgument: false, isChecked: conversionOpt?.Checked ?? false); @@ -8220,7 +8320,7 @@ private void VisitArgumentOutboundAssignmentsAndPostConditions( var parameterValue = new BoundParameter(argument.Syntax, parameter); var lValueType = result.LValueType; - trackNullableStateForAssignment(parameterValue, lValueType, MakeSlot(argument), parameterWithState, argument.IsSuppressed, parameterAnnotations); + trackNullableStateForAssignment(parameterValue, lValueType, MakeSlot(argument), parameterWithState, argument.IsSuppressed, parameterAnnotations, refKind, parameter); // check whether parameter would unsafely let a null out in the worse case if (!argument.IsSuppressed) @@ -8264,7 +8364,7 @@ private void VisitArgumentOutboundAssignmentsAndPostConditions( CheckDisallowedNullAssignment(parameterWithState, leftAnnotations, argument.Syntax); AdjustSetValue(argument, ref parameterWithState); - trackNullableStateForAssignment(parameterValue, lValueType, MakeSlot(argument), parameterWithState, argument.IsSuppressed, parameterAnnotations); + trackNullableStateForAssignment(parameterValue, lValueType, MakeSlot(argument), parameterWithState, argument.IsSuppressed, parameterAnnotations, refKind, parameter); // report warnings if parameter would unsafely let a null out in the worst case if (!argument.IsSuppressed) @@ -8302,9 +8402,9 @@ FlowAnalysisAnnotations notNullBasedOnParameters(FlowAnalysisAnnotations paramet return parameterAnnotations; } - void trackNullableStateForAssignment(BoundExpression parameterValue, TypeWithAnnotations lValueType, int targetSlot, TypeWithState parameterWithState, bool isSuppressed, FlowAnalysisAnnotations parameterAnnotations) + void trackNullableStateForAssignment(BoundExpression parameterValue, TypeWithAnnotations lValueType, int targetSlot, TypeWithState parameterWithState, bool isSuppressed, FlowAnalysisAnnotations parameterAnnotations, RefKind refKind, ParameterSymbol parameter) { - if (!IsConditionalState && !hasConditionalPostCondition(parameterAnnotations)) + if (!IsConditionalState && !hasConditionalPostCondition(parameterAnnotations, refKind, parameter)) { TrackNullableStateForAssignment(parameterValue, lValueType, targetSlot, parameterWithState.WithSuppression(isSuppressed)); } @@ -8315,7 +8415,7 @@ void trackNullableStateForAssignment(BoundExpression parameterValue, TypeWithAnn SetState(StateWhenTrue); // Note: the suppression applies over the post-condition attributes - TrackNullableStateForAssignment(parameterValue, lValueType, targetSlot, applyPostConditionsWhenTrue(parameterWithState, parameterAnnotations).WithSuppression(isSuppressed)); + TrackNullableStateForAssignment(parameterValue, lValueType, targetSlot, applyPostConditionsWhenTrue(parameterWithState, parameterAnnotations, refKind, parameter).WithSuppression(isSuppressed)); Debug.Assert(!IsConditionalState); var newWhenTrue = State.Clone(); @@ -8327,10 +8427,35 @@ void trackNullableStateForAssignment(BoundExpression parameterValue, TypeWithAnn } } - static bool hasConditionalPostCondition(FlowAnalysisAnnotations annotations) + static bool hasConditionalPostCondition(FlowAnalysisAnnotations annotations, RefKind refKind, ParameterSymbol parameter) { - return (((annotations & FlowAnalysisAnnotations.MaybeNullWhenTrue) != 0) ^ ((annotations & FlowAnalysisAnnotations.MaybeNullWhenFalse) != 0)) || - (((annotations & FlowAnalysisAnnotations.NotNullWhenTrue) != 0) ^ ((annotations & FlowAnalysisAnnotations.NotNullWhenFalse) != 0)); + if ((((annotations & FlowAnalysisAnnotations.MaybeNullWhenTrue) != 0) ^ ((annotations & FlowAnalysisAnnotations.MaybeNullWhenFalse) != 0)) || + (((annotations & FlowAnalysisAnnotations.NotNullWhenTrue) != 0) ^ ((annotations & FlowAnalysisAnnotations.NotNullWhenFalse) != 0))) + { + return true; + } + + return isUnionTryGetValueValue(refKind, parameter); + } + + static bool isUnionTryGetValueValue(RefKind refKind, ParameterSymbol parameter) + { + if (refKind == RefKind.Out && + parameter.ContainingSymbol is MethodSymbol + { + Name: WellKnownMemberNames.TryGetValueMethodName, + ReturnType.SpecialType: SpecialType.System_Boolean, + DeclaredAccessibility: Accessibility.Public, + RefKind: RefKind.None, + ParameterCount: 1, + ContainingType: { IsUnionType: true } unionType + } tryGetValue && + (object)tryGetValue.OriginalDefinition == Binder.GetUnionTypeTryGetValueMethod(unionType, parameter.Type)?.OriginalDefinition) // Looking for TryGetValue with exact type match at this call site + { + return true; + } + + return false; } static TypeWithState applyPostConditionsUnconditionally(TypeWithState typeWithState, FlowAnalysisAnnotations annotations) @@ -8350,7 +8475,7 @@ static TypeWithState applyPostConditionsUnconditionally(TypeWithState typeWithSt return typeWithState; } - static TypeWithState applyPostConditionsWhenTrue(TypeWithState typeWithState, FlowAnalysisAnnotations annotations) + static TypeWithState applyPostConditionsWhenTrue(TypeWithState typeWithState, FlowAnalysisAnnotations annotations, RefKind refKind, ParameterSymbol parameter) { bool notNullWhenTrue = (annotations & FlowAnalysisAnnotations.NotNullWhenTrue) != 0; bool maybeNullWhenTrue = (annotations & FlowAnalysisAnnotations.MaybeNullWhenTrue) != 0; @@ -8361,7 +8486,7 @@ static TypeWithState applyPostConditionsWhenTrue(TypeWithState typeWithState, Fl // [MaybeNull, NotNullWhen(true)] means [MaybeNullWhen(false)] return TypeWithState.Create(typeWithState.Type, NullableFlowState.MaybeDefault); } - else if (notNullWhenTrue) + else if (notNullWhenTrue || isUnionTryGetValueValue(refKind, parameter)) { return TypeWithState.Create(typeWithState.Type, NullableFlowState.NotNull); } @@ -9327,6 +9452,7 @@ private void TrackNullableStateOfTupleConversion( Conversion conversion, TypeSymbol targetType, TypeSymbol operandType, + bool fromExplicitCast, int slot, int valueSlot, AssignmentKind assignmentKind, @@ -9377,7 +9503,7 @@ void trackConvertedValue(FieldSymbol targetField, Conversion conversion, FieldSy int valueFieldSlot = GetOrCreateSlot(valueField, valueSlot); if (valueFieldSlot > 0) { - TrackNullableStateOfTupleConversion(conversionOpt, convertedNode, conversion, targetField.Type, valueField.Type, targetFieldSlot, valueFieldSlot, assignmentKind, parameterOpt, reportWarnings); + TrackNullableStateOfTupleConversion(conversionOpt, convertedNode, conversion, targetField.Type, valueField.Type, fromExplicitCast: fromExplicitCast, targetFieldSlot, valueFieldSlot, assignmentKind, parameterOpt, reportWarnings); } } } @@ -9421,6 +9547,64 @@ void trackConvertedValue(FieldSymbol targetField, Conversion conversion, FieldSy } } break; + + case ConversionKind.Union: + { + Debug.Assert(targetField.TypeWithAnnotations.Type.StrippedType() is NamedTypeSymbol { IsUnionType: true }); + int targetFieldSlot = GetOrCreateSlot(targetField, slot); + + TypeWithState valueFieldType = ApplyUnconditionalAnnotations(valueField.TypeWithAnnotations.ToTypeWithState(), GetRValueAnnotations(valueField)); + int valueFieldSlot = -1; + if (PossiblyNullableType(valueFieldType.Type)) + { + valueFieldSlot = GetOrCreateSlot(valueField, valueSlot); + if (valueFieldSlot > 0) + { + valueFieldType = TypeWithState.Create(valueFieldType.Type, GetState(ref this.State, valueFieldSlot)); + } + } + + var conversionOperand = new BoundValueForNullableAnalysis(convertedNode.Syntax, originalExpression: null, valueFieldType.Type); + int conversionOperandSlot = -1; + + if (targetFieldSlot > 0 || valueFieldSlot > 0) + { + conversionOperandSlot = GetOrCreatePlaceholderSlot(conversionOperand); + Debug.Assert(conversionOperandSlot > 0); + } + + if (conversionOperandSlot > 0) + { + SetState(ref this.State, conversionOperandSlot, valueFieldType.State); + } + + TypeWithState convertedType = VisitUnionConversion( + conversionOpt: null, + conversionOperand, + conversion, + targetField.TypeWithAnnotations, + valueFieldType, + fromExplicitCast: fromExplicitCast, + useLegacyWarnings: false, + assignmentKind, + parameterOpt, + reportTopLevelWarnings: reportWarnings, + reportRemainingWarnings: reportWarnings, + trackMembers: true, + targetInstanceSlotOpt: targetFieldSlot, + diagnosticLocation: (conversionOpt ?? convertedNode).Syntax.GetLocation()); + + if (targetFieldSlot > 0) + { + SetState(ref this.State, targetFieldSlot, convertedType.State); + } + + if (conversionOperandSlot > 0 && valueFieldSlot > 0) + { + SetState(ref this.State, valueFieldSlot, GetState(ref this.State, conversionOperandSlot)); + } + } + break; default: break; } @@ -9592,11 +9776,13 @@ private TypeWithState VisitConversion( bool extensionMethodThisArgument = false, Optional stateForLambda = default, bool trackMembers = false, + int targetInstanceSlotOpt = -1, Location? diagnosticLocation = null, ArrayBuilder? previousArgumentConversionResults = null) { Debug.Assert(!trackMembers || !IsConditionalState); Debug.Assert(conversionOperand != null); + Debug.Assert(conversionOpt is null || targetInstanceSlotOpt < 0); if (IsTargetTypedExpression(conversionOperand)) { @@ -9671,6 +9857,7 @@ private TypeWithState VisitConversion( #nullable enable case ConversionKind.AnonymousFunction: + Debug.Assert(targetInstanceSlotOpt < 0); if (conversionOperand is BoundLambda lambda) { var delegateType = targetType.GetDelegateType(); @@ -9709,8 +9896,29 @@ private TypeWithState VisitConversion( case ConversionKind.ExplicitUserDefined: case ConversionKind.ImplicitUserDefined: + Debug.Assert(targetInstanceSlotOpt < 0); return VisitUserDefinedConversion(conversionOpt, conversionOperand, conversion, targetTypeWithNullability, operandType, useLegacyWarnings, assignmentKind, parameterOpt, reportTopLevelWarnings, reportRemainingWarnings, getDiagnosticLocation()); + case ConversionKind.Union: + { + Debug.Assert(targetInstanceSlotOpt < 0); + return VisitUnionConversion( + conversionOpt, + conversionOperand, + conversion, + targetTypeWithNullability, + operandType, + fromExplicitCast: fromExplicitCast, + useLegacyWarnings, + assignmentKind, + parameterOpt, + reportTopLevelWarnings, + reportRemainingWarnings, + trackMembers: trackMembers, + targetInstanceSlotOpt: targetInstanceSlotOpt, + getDiagnosticLocation()); + } + case ConversionKind.ExplicitDynamic: case ConversionKind.ImplicitDynamic: resultState = getConversionResultState(operandType); @@ -9750,6 +9958,17 @@ private TypeWithState VisitConversion( goto case ConversionKind.Identity; case ConversionKind.Identity: + if (conversion.IsIdentity && trackMembers && (conversionOpt is { } ? !conversionOpt.Conversion.IsIdentity : (targetInstanceSlotOpt > 0))) + { + int operandSlot = MakeSlot(conversionOperand); + if (operandSlot > 0) + { + int containingSlot = conversionOpt is null ? targetInstanceSlotOpt : GetOrCreatePlaceholderSlot(conversionOpt); + Debug.Assert(containingSlot > 0); + TrackNullableStateForAssignment(conversionOperand, targetTypeWithNullability, containingSlot, operandType, operandSlot); + } + } + // If the operand is an explicit conversion, and this identity conversion // is converting to the same type including nullability, skip the conversion // to avoid reporting redundant warnings. Also check useLegacyWarnings @@ -9757,7 +9976,7 @@ private TypeWithState VisitConversion( // Don't skip the node when it's a user-defined conversion, as identity conversions // on top of user-defined conversions means that we're coming in from VisitUserDefinedConversion // and that any warnings caught by this recursive call of VisitConversion won't be redundant. - if (useLegacyWarnings && conversionOperand is BoundConversion operandConversion && !operandConversion.ConversionKind.IsUserDefinedConversion()) + if (useLegacyWarnings && conversionOperand is BoundConversion operandConversion && !operandConversion.ConversionKind.IsUserDefinedConversion()) // https://github.com/dotnet/roslyn/issues/82636: Follow up { var explicitType = operandConversion.ConversionGroupOpt?.ExplicitType; if (explicitType?.Equals(targetTypeWithNullability, TypeCompareKind.ConsiderEverything) == true) @@ -9769,6 +9988,9 @@ private TypeWithState VisitConversion( return operandType; } } + + targetInstanceSlotOpt = -1; + if (operandType.Type?.IsTupleType == true || conversionOperand.Kind == BoundKind.TupleLiteral) { goto case ConversionKind.ImplicitTuple; @@ -9777,6 +9999,7 @@ private TypeWithState VisitConversion( case ConversionKind.ImplicitReference: case ConversionKind.ExplicitReference: + Debug.Assert(targetInstanceSlotOpt < 0); // Inherit state from the operand. if (checkConversion) { @@ -9797,7 +10020,7 @@ private TypeWithState VisitConversion( int valueSlot = MakeSlot(conversionOperand); if (valueSlot > 0) { - int containingSlot = GetOrCreatePlaceholderSlot(conversionOpt); + int containingSlot = conversionOpt is null ? targetInstanceSlotOpt : GetOrCreatePlaceholderSlot(conversionOpt); Debug.Assert(containingSlot > 0); TrackNullableStateOfNullableValue(containingSlot, targetType, conversionOperand, underlyingType.ToTypeWithState(), valueSlot); } @@ -9835,6 +10058,7 @@ private TypeWithState VisitConversion( case ConversionKind.ImplicitTupleLiteral: case ConversionKind.ExplicitTupleLiteral: case ConversionKind.ExplicitTuple: + Debug.Assert(targetInstanceSlotOpt < 0); if (trackMembers) { Debug.Assert(conversionOperand != null); @@ -9842,25 +10066,47 @@ private TypeWithState VisitConversion( { case ConversionKind.ImplicitTuple: case ConversionKind.ExplicitTuple: - int valueSlot = MakeSlot(conversionOperand); - if (valueSlot > 0) { - int slot = GetOrCreatePlaceholderSlot(conversionOpt); - if (slot > 0) + int valueSlot = MakeSlot(conversionOperand); + if (valueSlot > 0) { - TrackNullableStateOfTupleConversion(conversionOpt, conversionOperand, conversion, targetType, operandType.Type, slot, valueSlot, assignmentKind, parameterOpt, reportWarnings: reportRemainingWarnings); + int slot = GetOrCreatePlaceholderSlot(conversionOpt); + if (slot > 0) + { + TrackNullableStateOfTupleConversion(conversionOpt, conversionOperand, conversion, targetType, operandType.Type, fromExplicitCast: fromExplicitCast, slot, valueSlot, assignmentKind, parameterOpt, reportWarnings: reportRemainingWarnings); + } } + break; } - break; } } if (checkConversion && !targetType.IsErrorType()) { // https://github.com/dotnet/roslyn/issues/29699: Report warnings for user-defined conversions on tuple elements. - conversion = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument, isChecked: conversionOpt?.Checked ?? false); - canConvertNestedNullability = conversion.Exists; + canConvertNestedNullability = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument, isChecked: conversionOpt?.Checked ?? false).Exists; + + if (canConvertNestedNullability && trackMembers && conversionOpt is { } && conversionOperand.Type.Equals(conversionOpt.Type, TypeCompareKind.AllIgnoreOptions)) + { + Debug.Assert(conversionOperand != null); + switch (conversion.Kind) + { + case ConversionKind.ImplicitTupleLiteral: + case ConversionKind.ExplicitTupleLiteral: + int valueSlot = MakeSlot(conversionOperand); + if (valueSlot > 0) + { + int slot = GetOrCreatePlaceholderSlot(conversionOpt); + if (slot > 0) + { + InheritNullableStateOfTrackableType(slot, valueSlot, slot); + } + } + break; + } + } } + resultState = NullableFlowState.NotNull; break; @@ -9876,8 +10122,8 @@ private TypeWithState VisitConversion( case ConversionKind.InlineArray: if (checkConversion) { - conversion = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument, isChecked: conversionOpt?.Checked ?? false); - canConvertNestedNullability = conversion.Exists; + Conversion generated = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument, isChecked: conversionOpt?.Checked ?? false); + canConvertNestedNullability = generated.Exists; } break; @@ -9885,10 +10131,9 @@ private TypeWithState VisitConversion( case ConversionKind.ExplicitSpan: if (checkConversion) { - var previousKind = conversion.Kind; - conversion = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument, isChecked: conversionOpt?.Checked ?? false); + Conversion generated = GenerateConversion(_conversions, conversionOperand, operandType.Type, targetType, fromExplicitCast, extensionMethodThisArgument, isChecked: conversionOpt?.Checked ?? false); // We do not want user-defined conversions to relax nullability, so we consider only span conversions. - canConvertNestedNullability = conversion.Exists && conversion.IsSpan; + canConvertNestedNullability = generated.Exists && generated.IsSpan; } break; @@ -10280,6 +10525,156 @@ private TypeWithState VisitUserDefinedConversion( return operandType; } + private TypeWithState VisitUnionConversion( + BoundConversion? conversionOpt, + BoundExpression conversionOperand, + Conversion conversion, + TypeWithAnnotations targetTypeWithNullability, + TypeWithState operandType, + bool fromExplicitCast, + bool useLegacyWarnings, + AssignmentKind assignmentKind, + ParameterSymbol? parameterOpt, + bool reportTopLevelWarnings, + bool reportRemainingWarnings, + bool trackMembers, + int targetInstanceSlotOpt, + Location diagnosticLocation) + { + Debug.Assert(!IsConditionalState); + Debug.Assert(conversionOperand != null); + Debug.Assert(targetTypeWithNullability.HasType); + Debug.Assert(diagnosticLocation != null); + Debug.Assert(conversion.IsUnion); + Debug.Assert(conversionOpt is null || targetInstanceSlotOpt < 0); + + UserDefinedConversionAnalysis analysis = conversion.BestUnionConversionAnalysis; + + Debug.Assert(analysis.Kind == UserDefinedConversionAnalysisKind.ApplicableInNormalForm); + Debug.Assert(analysis.Operator is { MethodKind: MethodKind.Constructor, ParameterCount: 1 }); + Debug.Assert(TypeSymbol.Equals(analysis.FromType, analysis.Operator.GetParameterType(0), TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(TypeSymbol.Equals(targetTypeWithNullability.Type.StrippedType(), analysis.Operator.ContainingType, TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(TypeSymbol.Equals(targetTypeWithNullability.Type.StrippedType(), analysis.ToType, TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(analysis.TargetConversion is { IsIdentity: true } or { IsNullable: true, IsImplicit: true }); + + BoundConversion? sourceToParameterConversion = null; + BoundConversion? unionConstructionConversion = null; + + if (conversionOpt is { ConversionGroupOpt.Conversion.IsUnion: true }) + { + conversionOpt.TryGetUnionConversionParts(out sourceToParameterConversion, out unionConstructionConversion, out _); + Debug.Assert(unionConstructionConversion is not null, "Unexpected conversion group structure for union conversion."); + Debug.Assert(sourceToParameterConversion is not null || analysis.SourceConversion.IsIdentity); + } + else + { + Debug.Assert(conversionOpt is null); + } + + MethodSymbol constructor = analysis.Operator; + TypeSymbol targetType = targetTypeWithNullability.Type; + var toType = (NamedTypeSymbol)targetType.StrippedType(); + +#if DEBUG + bool found = false; +#endif + foreach (var ctor in toType.InstanceConstructors) + { + if (ctor.OriginalDefinition == (object)constructor.OriginalDefinition) + { + constructor = ctor; +#if DEBUG + found = true; +#endif + break; + } + } + +#if DEBUG + Debug.Assert(found); +#endif + + // operand -> conversion "from" type + var parameter = constructor.Parameters[0]; + var parameterAnnotations = GetParameterAnnotations(parameter); + var parameterType = ApplyLValueAnnotations(parameter.TypeWithAnnotations, parameterAnnotations); + + Debug.Assert(TypeSymbol.Equals(analysis.FromType, parameterType.Type, TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(TypeSymbol.Equals(toType, analysis.ToType, TypeCompareKind.AllIgnoreOptions)); + + operandType = VisitConversion( + conversionOpt: null, + conversionOperand, + analysis.SourceConversion, + parameterType, + operandType, + checkConversion: true, + fromExplicitCast: false, + useLegacyWarnings, + AssignmentKind.Argument, + parameterOpt: parameter, + reportTopLevelWarnings, + reportRemainingWarnings, + trackMembers: false, + diagnosticLocation: diagnosticLocation); + + Debug.Assert(operandType.Type is object); + + if (CheckDisallowedNullAssignment(operandType, parameterAnnotations, conversionOperand.Syntax)) + { + LearnFromNonNullTest(conversionOperand, ref State); + } + + LearnFromPostConditions(conversionOperand, parameterAnnotations); + + if (sourceToParameterConversion is { }) + { + SetAnalyzedNullability(sourceToParameterConversion, new VisitResult(operandType, operandType.ToTypeWithAnnotations(compilation))); + } + + // toType -> target type + var unionInstance = new BoundValueForNullableAnalysis((unionConstructionConversion ?? conversionOperand).Syntax, unionConstructionConversion, toType); + var unionTypeWithState = TypeWithState.Create(toType, NullableFlowState.NotNull); + + if (unionConstructionConversion is { }) + { + SetAnalyzedNullability(unionConstructionConversion, new VisitResult(unionTypeWithState, unionTypeWithState.ToTypeWithAnnotations(compilation))); + } + + if (trackMembers && (conversionOpt is { } || targetInstanceSlotOpt > 0) && + targetTypeWithNullability.Type.StrippedType() is NamedTypeSymbol { IsUnionType: true } unionType && + Binder.GetUnionTypeValuePropertyNoUseSiteDiagnostics(unionType) is { } valueProperty) + { + // When a union constructor is called through a union conversion, the new union's Value gets the null state of the incoming value. + Debug.Assert(conversionOperand != null); + int containingSlot = GetOrCreatePlaceholderSlot(unionInstance); + Debug.Assert(containingSlot > 0); + + SetUnionValueStateFromConstructorArgument(conversionOperand, operandType, containingSlot, valueProperty); + } + + operandType = VisitConversion( + conversionOpt: conversionOpt, + conversionOperand: unionInstance, + analysis.TargetConversion, + targetTypeWithNullability: targetTypeWithNullability, + operandType: unionTypeWithState, + checkConversion: true, + fromExplicitCast: fromExplicitCast, + useLegacyWarnings, + assignmentKind, + parameterOpt: null, + reportTopLevelWarnings, + reportRemainingWarnings, + trackMembers: trackMembers, + targetInstanceSlotOpt: targetInstanceSlotOpt, + diagnosticLocation: diagnosticLocation); + + // Update constructor symbol in the tree: see https://github.com/dotnet/roslyn/issues/29605. + + return TypeWithState.Create(targetType, NullableFlowState.NotNull); + } + private void SnapshotWalkerThroughConversionGroup(BoundExpression conversionExpression, BoundExpression convertedNode) { if (_snapshotBuilderOpt is null) @@ -10303,9 +10698,14 @@ private void TrackAnalyzedNullabilityThroughConversionGroup(TypeWithState result { var visitResult = new VisitResult(resultType, resultType.ToTypeWithAnnotations(compilation)); var conversionGroup = conversionOpt?.ConversionGroupOpt; - while (conversionOpt != null && conversionOpt != convertedNode) + while (conversionOpt != null && conversionOpt != convertedNode && + conversionOpt != (convertedNode as BoundValueForNullableAnalysis)?.OriginalExpression) { Debug.Assert(conversionOpt.ConversionGroupOpt == conversionGroup); + if (conversionOpt.ConversionGroupOpt != conversionGroup) + { + break; + } // https://github.com/dotnet/roslyn/issues/35046 // SetAnalyzedNullability will drop the type if the visitResult.RValueType.Type differs from conversionOpt.Type. @@ -11340,9 +11740,17 @@ private ImmutableArray GetDeconstructionRightParts(BoundExpress // Analyze operator call properly (honoring [Disallow|Allow|Maybe|NotNull] attribute annotations) https://github.com/dotnet/roslyn/issues/32671 // https://github.com/dotnet/roslyn/issues/29961 Update conversion method based on operand type. - if (node.OperandConversion is BoundConversion { Conversion: var operandConversion } && operandConversion.IsUserDefined && operandConversion.Method?.ParameterCount == 1) + if (node.OperandConversion is BoundConversion { Conversion: ({ IsUserDefined: true } or { IsUnion: true }) and { Method.ParameterCount: 1 } operandConversion }) { - targetTypeOfOperandConversion = operandConversion.Method.ReturnTypeWithAnnotations; + if (operandConversion.IsUserDefined) + { + targetTypeOfOperandConversion = operandConversion.Method.ReturnTypeWithAnnotations; + } + else + { + targetTypeOfOperandConversion = TypeWithAnnotations.Create(node.OperandConversion.Type, nullableAnnotation: NullableAnnotation.NotAnnotated); // https://github.com/dotnet/roslyn/issues/82636: Add coverage + // https://github.com/dotnet/roslyn/issues/82636: Track something for the underlying value? + } } else if (incrementOperator is object) { @@ -11785,10 +12193,19 @@ private Symbol VisitMemberAccess(BoundExpression node, BoundExpression? receiver var receiverType = (receiverOpt != null) ? VisitRvalueWithState(receiverOpt) : default; SpecialMember? nullableOfTMember = null; + PropertySymbol? unionValue = null; if (member.RequiresInstanceReceiver()) { member = AsMemberOfType(receiverType.Type, member); nullableOfTMember = GetNullableOfTMember(member); + + if (member is PropertySymbol { Name: WellKnownMemberNames.HasValuePropertyName } property && + receiverType.Type is NamedTypeSymbol { IsUnionType: true } unionType && + Binder.IsUnionTypeHasValueProperty(unionType, property)) + { + unionValue = Binder.GetUnionTypeValuePropertyNoUseSiteDiagnostics(unionType); + } + // https://github.com/dotnet/roslyn/issues/30598: For l-values, mark receiver as not null // after RHS has been visited, and only if the receiver has not changed. bool skipReceiverNullCheck = nullableOfTMember != SpecialMember.System_Nullable_T_get_Value; @@ -11812,13 +12229,23 @@ private Symbol VisitMemberAccess(BoundExpression node, BoundExpression? receiver } Debug.Assert(!IsConditionalState); - if (nullableOfTMember == SpecialMember.System_Nullable_T_get_HasValue && !(receiverOpt is null)) + if (receiverOpt is not null) { - int containingSlot = MakeSlot(receiverOpt); - if (containingSlot > 0) + int slotToSet = -1; + + if (nullableOfTMember == SpecialMember.System_Nullable_T_get_HasValue) + { + slotToSet = MakeSlot(receiverOpt); + } + else if (unionValue is not null) + { + slotToSet = MakeMemberSlot(receiverOpt, unionValue); + } + + if (slotToSet > 0) { Split(); - SetState(ref this.StateWhenTrue, containingSlot, NullableFlowState.NotNull); + SetState(ref this.StateWhenTrue, slotToSet, NullableFlowState.NotNull); } } @@ -12068,23 +12495,13 @@ private void VisitForEachExpression( // Analyze `await DisposeAsync()` if (enumeratorInfoOpt is { NeedsDisposal: true, DisposeAwaitableInfo: BoundAwaitableInfo awaitDisposalInfo }) { - var disposalPlaceholder = awaitDisposalInfo.AwaitableInstancePlaceholder; - bool addedPlaceholder = false; - if (enumeratorInfoOpt.PatternDisposeInfo is { Method: var originalDisposeMethod }) // no statically known Dispose method if doing a runtime check + var patternDisposeMethod = enumeratorInfoOpt.PatternDisposeInfo?.Method; + if (patternDisposeMethod is not null) { - Debug.Assert(disposalPlaceholder is not null); - var disposeAsyncMethod = (MethodSymbol)AsMemberOfType(reinferredGetEnumeratorMethod.ReturnType, originalDisposeMethod); - var result = new VisitResult(GetReturnTypeWithState(disposeAsyncMethod), disposeAsyncMethod.ReturnTypeWithAnnotations); - AddPlaceholderReplacement(disposalPlaceholder, disposalPlaceholder, result); - addedPlaceholder = true; + patternDisposeMethod = (MethodSymbol)AsMemberOfType(reinferredGetEnumeratorMethod.ReturnType, patternDisposeMethod); } - Visit(awaitDisposalInfo); - - if (addedPlaceholder) - { - RemovePlaceholderReplacement(disposalPlaceholder!); - } + VisitAwaitableInfoForUsing(awaitDisposalInfo, patternDisposeMethod); } } @@ -12570,20 +12987,18 @@ protected override void AfterLeftChildOfBinaryLogicalOperatorHasBeenVisited(Boun Visit(awaitableInfo); RemovePlaceholderReplacement(placeholder); - if (node.Type.IsValueType || node.HasErrors || awaitableInfo.GetResult is null) + if (awaitableInfo is { GetResult: null, RuntimeAsyncAwaitCall: not null }) + { + // This is AsyncHelpers.Await. We can directly use `_visitResult` + SetResult(node, _visitResult, updateAnalyzedNullability: true, isLvalue: false); + } + else if (awaitableInfo.GetResult is null) { SetNotNullResult(node); } else { - // It is possible for the awaiter type returned from GetAwaiter to not be a named type. e.g. it could be a type parameter. - // Proper handling of this is additional work which only benefits a very uncommon scenario, - // so we will just use the originally bound GetResult method in this case. - var getResult = awaitableInfo.GetResult; - var reinferredGetResult = _visitResult.RValueType.Type is NamedTypeSymbol taskAwaiterType - ? getResult.OriginalDefinition.AsMember(taskAwaiterType) - : getResult; - + var reinferredGetResult = (MethodSymbol)AsMemberOfType(_visitResult.RValueType.Type, awaitableInfo.GetResult); SetResultType(node, reinferredGetResult.ReturnTypeWithAnnotations.ToTypeWithState()); } @@ -13193,6 +13608,12 @@ private void VisitThrow(BoundExpression? expr) return null; } + public override BoundNode? VisitValueForNullableAnalysis(BoundValueForNullableAnalysis node) + { + ExceptionUtilities.UnexpectedValue(node); + return base.VisitValueForNullableAnalysis(node); + } + public override BoundNode? VisitDeconstructValuePlaceholder(BoundDeconstructValuePlaceholder node) { // These placeholders don't yet follow proper placeholder discipline @@ -13211,8 +13632,6 @@ private void VisitThrow(BoundExpression? expr) public override BoundNode? VisitAwaitableValuePlaceholder(BoundAwaitableValuePlaceholder node) { - // These placeholders don't always follow proper placeholder discipline yet - AssertPlaceholderAllowedWithoutRegistration(node); VisitPlaceholderWithReplacement(node); return null; } @@ -13234,8 +13653,55 @@ private void VisitPlaceholderWithReplacement(BoundValuePlaceholderBase node) public override BoundNode? VisitAwaitableInfo(BoundAwaitableInfo node) { - Visit(node.AwaitableInstancePlaceholder); - Visit(node.GetAwaiter); + if (node is { GetAwaiter: null, GetResult: null, RuntimeAsyncAwaitCall: null, RuntimeAsyncAwaitCallPlaceholder: null }) + { + if (node.AwaitableInstancePlaceholder is not null) + { + // Ensure that the placeholder has an analyzed nullability for the rewriter + SetNotNullResult(node.AwaitableInstancePlaceholder); + } + + SetInvalidResult(); + + // Error scenario + return null; + } + + Debug.Assert(node.AwaitableInstancePlaceholder is not null); + + if (node.RuntimeAsyncAwaitCall is not null) + { + Debug.Assert(node.RuntimeAsyncAwaitCallPlaceholder is not null); + + if (node.GetAwaiter is null) + { + // This is the simple case of just `AsyncHelpers.Await(expr)`. AwaitableInstancePlaceholder and RuntimeAsyncAwaitCallPlaceholder + // refer to the same value. + Debug.Assert(node.GetResult is null); + VisitPlaceholderWithReplacement(node.AwaitableInstancePlaceholder); + AddPlaceholderReplacement(node.RuntimeAsyncAwaitCallPlaceholder, node.AwaitableInstancePlaceholder, _visitResult); + Visit(node.RuntimeAsyncAwaitCall); + RemovePlaceholderReplacement(node.RuntimeAsyncAwaitCallPlaceholder); + } + else + { + // More complicated case of `AsyncHelpers.AwaitAwaiter/UnsafeAwaitAwaiter`. We need to visit GetAwaiter first to get the nullability + // result for the awaiter, which we then save. It is used both here, as the state of the RuntimeAsyncAwaitCallPlaceholder replacement, + // and also for the caller to be able to re-infer GetResult with nullability. + Visit(node.GetAwaiter); + var getAwaiterResult = _visitResult; + AddPlaceholderReplacement(node.RuntimeAsyncAwaitCallPlaceholder, node.GetAwaiter, getAwaiterResult); + Visit(node.RuntimeAsyncAwaitCall); + RemovePlaceholderReplacement(node.RuntimeAsyncAwaitCallPlaceholder); + _visitResult = getAwaiterResult; + } + } + else + { + // Not runtime async + Visit(node.GetAwaiter); + } + return null; } diff --git a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker_Patterns.cs b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker_Patterns.cs index 4eba7b9a55cf..ec30a662341c 100644 --- a/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker_Patterns.cs +++ b/src/Compilers/CSharp/Portable/FlowAnalysis/NullableWalker_Patterns.cs @@ -322,7 +322,7 @@ protected override void VisitSwitchSection(BoundSwitchSection node, bool isLastS TakeIncrementalSnapshot(label); VisitForRewriting(label.Pattern); - if (!State.Reachable && label.WhenClause != null) + if (!LabelState(label.Label).Reachable && label.WhenClause != null) { // Unreachable when clauses are not visited in `LearnFromDecisionDag`. VisitForRewriting(label.WhenClause); @@ -407,6 +407,7 @@ public PossiblyConditionalState Clone() // Note we customize equality in BoundDagTemp var tempMap = PooledDictionary.GetInstance(); + var reinferredPropertyMap = PooledDictionary.GetInstance(); Debug.Assert(isDerivedType(NominalSlotType(originalInputSlot), expressionTypeWithState.Type)); tempMap.Add(rootTemp, (originalInputSlot, expressionTypeWithState.Type)); @@ -444,105 +445,39 @@ public PossiblyConditionalState Clone() // https://github.com/dotnet/roslyn/issues/34232 // We may need to recompute the Deconstruct method for a deconstruction if // the receiver type has changed (e.g. its nested nullability). - var method = e.DeconstructMethod; - int extensionExtra = method.RequiresInstanceReceiver ? 0 : 1; - for (int i = 0; i < method.ParameterCount - extensionExtra; i++) + ArrayBuilder outParamTemps = e.MakeOutParameterTemps(); + foreach (var output in outParamTemps) { - var parameterType = method.Parameters[i + extensionExtra].TypeWithAnnotations; - var output = new BoundDagTemp(e.Syntax, parameterType.Type, e, i); - int outputSlot = makeDagTempSlot(parameterType, output); - Debug.Assert(outputSlot > 0); - addToTempMap(output, outputSlot, parameterType.Type); + int outputSlot = getOrMakeAndRegisterDagTempSlot(output); } + outParamTemps.Free(); break; } case BoundDagTypeEvaluation e: { - var output = new BoundDagTemp(e.Syntax, e.Type, e); - var discardedUseSiteInfo = CompoundUseSiteInfo.Discarded; - int outputSlot; - switch (_conversions.WithNullability(false).ClassifyConversionFromType(inputType, e.Type, isChecked: false, ref discardedUseSiteInfo).Kind) - { - case ConversionKind.Identity: - case ConversionKind.ImplicitReference: - outputSlot = inputSlot; - break; - case ConversionKind.ExplicitNullable when AreNullableAndUnderlyingTypes(inputType, e.Type, out _): - outputSlot = GetNullableOfTValueSlot(inputType, inputSlot, out _, forceSlotEvenIfEmpty: true); - if (outputSlot < 0) - goto default; - break; - default: - outputSlot = makeDagTempSlot(TypeWithAnnotations.Create(e.Type, NullableAnnotation.NotAnnotated), output); - break; - } + var output = e.MakeResultTemp(); + int outputSlot = getOrMakeAndRegisterDagTempSlot(output); Debug.Assert(!IsConditionalState); Unsplit(); SetState(ref State, outputSlot, NullableFlowState.NotNull); - addToTempMap(output, outputSlot, e.Type); break; } case BoundDagFieldEvaluation e: { - Debug.Assert(inputSlot > 0); - var field = (FieldSymbol)AsMemberOfType(inputType, e.Field); - var type = field.TypeWithAnnotations; - var output = new BoundDagTemp(e.Syntax, type.Type, e); - int outputSlot = -1; - var originalTupleElement = e.Input.IsOriginalInput && !originalInputElementSlots.IsDefault - ? field - : null; - if (originalTupleElement is not null) - { - // Re-use the slot of the element/expression if possible - outputSlot = originalInputElementSlots[originalTupleElement.TupleElementIndex]; - } - if (outputSlot <= 0) - { - outputSlot = GetOrCreateSlot(field, inputSlot, forceSlotEvenIfEmpty: true); - - if (originalTupleElement is not null && outputSlot > 0) - { - // The expression in the tuple could not be assigned a slot (for example, `a?.b`), - // so we had to create a slot for the tuple element instead. - // We'll remember that so that we can apply any learnings to the expression. -#pragma warning disable CA1854 //Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup - if (!originalInputMap.ContainsKey(outputSlot)) -#pragma warning restore CA1854 - { - originalInputMap.Add(outputSlot, - ((BoundTupleExpression)expression).Arguments[originalTupleElement.TupleElementIndex]); - } - else - { - Debug.Assert(originalInputMap[outputSlot] == ((BoundTupleExpression)expression).Arguments[originalTupleElement.TupleElementIndex]); - } - } - } - if (outputSlot <= 0) - { - outputSlot = makeDagTempSlot(type, output); - } - + var output = e.MakeResultTemp(); + int outputSlot = getOrMakeAndRegisterDagTempSlot(output); Debug.Assert(outputSlot > 0); - addToTempMap(output, outputSlot, type.Type); break; } case BoundDagPropertyEvaluation e: { Debug.Assert(inputSlot > 0); - var property = e.Property.IsExtensionBlockMember() - ? ReInferAndVisitExtensionPropertyAccess(e, e.Property, new BoundExpressionWithNullability(e.Syntax, expression, NullableAnnotation.NotAnnotated, inputType)).Member - : (PropertySymbol)AsMemberOfType(inputType, e.Property); + var property = getReInferredProperty(inputType, e); var type = property.TypeWithAnnotations; - var output = new BoundDagTemp(e.Syntax, type.Type, e); - int outputSlot = GetOrCreateSlot(property, inputSlot, forceSlotEvenIfEmpty: true); - if (outputSlot <= 0) - { - outputSlot = makeDagTempSlot(type, output); - } + + var output = e.MakeResultTemp(); + int outputSlot = getOrMakeAndRegisterDagTempSlot(output); Debug.Assert(outputSlot > 0); - addToTempMap(output, outputSlot, type.Type); if (property.GetMethod is not null) { @@ -553,34 +488,44 @@ public PossiblyConditionalState Clone() break; } case BoundDagIndexEvaluation e: - addTemp(e, e.Property.Type); - break; + { + var output = e.MakeResultTemp(); + int outputSlot = getOrMakeAndRegisterDagTempSlot(output); + Debug.Assert(outputSlot > 0); + break; + } case BoundDagIndexerEvaluation e: { // tDest = tSource[index] - Debug.Assert(inputSlot > 0); TypeWithAnnotations type = getIndexerOutputType(inputType, e.IndexerAccess, isSlice: false); - var output = new BoundDagTemp(e.Syntax, type.Type, e); - var outputSlot = makeDagTempSlot(type, output); + var output = e.MakeResultTemp(); + var outputSlot = getOrMakeAndRegisterDagTempSlot(output); Debug.Assert(outputSlot > 0); TrackNullableStateForAssignment(valueOpt: null, type, outputSlot, type.ToTypeWithState()); - addToTempMap(output, outputSlot, type.Type); break; } case BoundDagSliceEvaluation e: { // tDest = tSource[range] - Debug.Assert(inputSlot > 0); TypeWithAnnotations type = getIndexerOutputType(inputType, e.IndexerAccess, isSlice: true); - var output = new BoundDagTemp(e.Syntax, type.Type, e); - var outputSlot = makeDagTempSlot(type, output); + var output = e.MakeResultTemp(); + var outputSlot = getOrMakeAndRegisterDagTempSlot(output); Debug.Assert(outputSlot > 0); - addToTempMap(output, outputSlot, type.Type); SetState(ref this.State, outputSlot, NullableFlowState.NotNull); // Slice value is assumed to be never null break; } case BoundDagAssignmentEvaluation e: - break; + { + int outputSlot = getOrMakeAndRegisterDagTempSlot(e.Target); + if (outputSlot > 0) + { + var inputState = GetState(ref this.State, inputSlot); + var inputTypeWithState = TypeWithState.Create(inputType, inputState); + TrackNullableStateForAssignment(valueOpt: null, inputTypeWithState.ToTypeWithAnnotations(compilation), outputSlot, inputTypeWithState, inputSlot); + } + + break; + } default: throw ExceptionUtilities.UnexpectedValue(p.Evaluation.Kind); } @@ -725,9 +670,171 @@ public PossiblyConditionalState Clone() SetUnreachable(); // the decision dag is always complete (no fall-through) originalInputMap.Free(); tempMap.Free(); + reinferredPropertyMap.Free(); nodeStateMap.Free(); return labelStateMap; + int getOrMakeAndRegisterDagTempSlot(BoundDagTemp output) + { + if (tempMap.TryGetValue(output, out var targetSlotAndType)) + { + return targetSlotAndType.slot; + } + + var evaluation = output.Source; + getOrMakeAndRegisterDagTempSlot(evaluation.Input); + (int inputSlot, TypeSymbol inputType) = tempMap.TryGetValue(evaluation.Input, out var slotAndType) ? slotAndType : throw ExceptionUtilities.Unreachable(); + Debug.Assert(inputSlot > 0); + + switch (evaluation) + { + case BoundDagDeconstructEvaluation e: + { + // https://github.com/dotnet/roslyn/issues/34232 + // We may need to recompute the Deconstruct method for a deconstruction if + // the receiver type has changed (e.g. its nested nullability). + var method = e.DeconstructMethod; + int extensionExtra = method.RequiresInstanceReceiver ? 0 : 1; + var parameterType = method.Parameters[output.Index + extensionExtra].TypeWithAnnotations; + int outputSlot = makeDagTempSlot(parameterType, output); + Debug.Assert(outputSlot > 0); + addToTempMap(output, outputSlot, parameterType.Type); + return outputSlot; + } + case BoundDagTypeEvaluation e: + { + var discardedUseSiteInfo = CompoundUseSiteInfo.Discarded; + int outputSlot; + switch (_conversions.WithNullability(false).ClassifyConversionFromType(e.Input.Type, e.Type, isChecked: false, ref discardedUseSiteInfo).Kind) + { + case ConversionKind.Identity: + case ConversionKind.ImplicitReference: + outputSlot = inputSlot; + break; + case ConversionKind.ExplicitNullable when AreNullableAndUnderlyingTypes(inputType, e.Type, out _): + outputSlot = GetNullableOfTValueSlot(inputType, inputSlot, out _, forceSlotEvenIfEmpty: true); + if (outputSlot < 0) + goto default; + break; + default: + outputSlot = makeDagTempSlot(TypeWithAnnotations.Create(e.Type, NullableAnnotation.NotAnnotated), output); + break; + } + + addToTempMap(output, outputSlot, e.Type); + return outputSlot; + } + case BoundDagFieldEvaluation e: + { + Debug.Assert(inputSlot > 0); + var field = (FieldSymbol)AsMemberOfType(inputType, e.Field); + var type = field.TypeWithAnnotations; + int outputSlot = -1; + var originalTupleElement = e.Input.IsOriginalInput && !originalInputElementSlots.IsDefault + ? field + : null; + if (originalTupleElement is not null) + { + // Re-use the slot of the element/expression if possible + outputSlot = originalInputElementSlots[originalTupleElement.TupleElementIndex]; + } + if (outputSlot <= 0) + { + outputSlot = GetOrCreateSlot(field, inputSlot, forceSlotEvenIfEmpty: true); + + if (originalTupleElement is not null && outputSlot > 0) + { + // The expression in the tuple could not be assigned a slot (for example, `a?.b`), + // so we had to create a slot for the tuple element instead. + // We'll remember that so that we can apply any learnings to the expression. +#pragma warning disable CA1854 //Prefer a 'TryGetValue' call over a Dictionary indexer access guarded by a 'ContainsKey' check to avoid double lookup + if (!originalInputMap.ContainsKey(outputSlot)) +#pragma warning restore CA1854 + { + originalInputMap.Add(outputSlot, + ((BoundTupleExpression)expression).Arguments[originalTupleElement.TupleElementIndex]); + } + else + { + Debug.Assert(originalInputMap[outputSlot] == ((BoundTupleExpression)expression).Arguments[originalTupleElement.TupleElementIndex]); + } + } + } + if (outputSlot <= 0) + { + outputSlot = makeDagTempSlot(type, output); + } + + Debug.Assert(outputSlot > 0); + addToTempMap(output, outputSlot, type.Type); + return outputSlot; + } + case BoundDagPropertyEvaluation e: + { + Debug.Assert(inputSlot > 0); + var property = getReInferredProperty(inputType, e); + var type = property.TypeWithAnnotations; + + int outputSlot = GetOrCreateSlot(property, inputSlot, forceSlotEvenIfEmpty: true); + if (outputSlot <= 0) + { + outputSlot = makeDagTempSlot(type, output); + } + Debug.Assert(outputSlot > 0); + addToTempMap(output, outputSlot, type.Type); + return outputSlot; + } + case BoundDagIndexEvaluation e: + { + var type = TypeWithAnnotations.Create(e.Property.Type, NullableAnnotation.Annotated); + int outputSlot = makeDagTempSlot(type, output); + Debug.Assert(outputSlot > 0); + addToTempMap(output, outputSlot, type.Type); + return outputSlot; + } + case BoundDagIndexerEvaluation e: + { + // tDest = tSource[index] + Debug.Assert(inputSlot > 0); + TypeWithAnnotations type = getIndexerOutputType(inputType, e.IndexerAccess, isSlice: false); + + var outputSlot = makeDagTempSlot(type, output); + Debug.Assert(outputSlot > 0); + addToTempMap(output, outputSlot, type.Type); + return outputSlot; + } + case BoundDagSliceEvaluation e: + { + // tDest = tSource[range] + Debug.Assert(inputSlot > 0); + TypeWithAnnotations type = getIndexerOutputType(inputType, e.IndexerAccess, isSlice: true); + + var outputSlot = makeDagTempSlot(type, output); + Debug.Assert(outputSlot > 0); + addToTempMap(output, outputSlot, type.Type); + return outputSlot; + } + case BoundDagAssignmentEvaluation e: + default: + throw ExceptionUtilities.UnexpectedValue(evaluation.Kind); + } + } + + PropertySymbol getReInferredProperty(TypeSymbol inputType, BoundDagPropertyEvaluation e) + { + if (reinferredPropertyMap.TryGetValue(e, out PropertySymbol property)) + { + return property; + } + + property = e.Property.IsExtensionBlockMember() + ? ReInferAndVisitExtensionPropertyAccess(e, e.Property, new BoundExpressionWithNullability(e.Syntax, expression, NullableAnnotation.NotAnnotated, inputType)).Member + : (PropertySymbol)AsMemberOfType(inputType, e.Property); + + reinferredPropertyMap.Add(e, property); + return property; + } + void learnFromNonNullTest(int inputSlot, ref LocalState state) { if (stateWhenNotNullOpt is { } stateWhenNotNull && inputSlot == originalInputSlot) @@ -836,15 +943,6 @@ int makeDagTempSlot(TypeWithAnnotations type, BoundDagTemp temp) return GetOrCreatePlaceholderSlot(slotKey, type); } - void addTemp(BoundDagEvaluation e, TypeSymbol t, int index = 0) - { - var type = TypeWithAnnotations.Create(t, NullableAnnotation.Annotated); - var output = new BoundDagTemp(e.Syntax, type.Type, e, index: index); - int outputSlot = makeDagTempSlot(type, output); - Debug.Assert(outputSlot > 0); - addToTempMap(output, outputSlot, type.Type); - } - static TypeWithAnnotations getIndexerOutputType(TypeSymbol inputType, BoundExpression e, bool isSlice) { return e switch diff --git a/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs b/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs index 2285ed4ef8de..9023d28cbd3f 100644 --- a/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/BoundNodes.xml.Generated.cs @@ -170,6 +170,7 @@ internal enum BoundKind : byte DagIndexerEvaluation, DagSliceEvaluation, DagAssignmentEvaluation, + DagPassThroughEvaluation, SwitchSection, SwitchLabel, SequencePointExpression, @@ -232,6 +233,7 @@ internal enum BoundKind : byte StringInsert, IsPatternExpression, ConstantPattern, + PatternWithUnionMatching, DiscardPattern, DeclarationPattern, RecursivePattern, @@ -253,6 +255,7 @@ internal enum BoundKind : byte NonConstructorMethodBody, ConstructorMethodBody, ExpressionWithNullability, + ValueForNullableAnalysis, WithExpression, } @@ -4955,7 +4958,7 @@ protected BoundSwitchExpression(BoundKind kind, SyntaxNode syntax, BoundExpressi internal sealed partial class BoundSwitchExpressionArm : BoundNode { - public BoundSwitchExpressionArm(SyntaxNode syntax, ImmutableArray locals, BoundPattern pattern, BoundExpression? whenClause, BoundExpression value, LabelSymbol label, bool hasErrors = false) + public BoundSwitchExpressionArm(SyntaxNode syntax, ImmutableArray locals, BoundPattern pattern, bool hasUnionMatching, BoundExpression? whenClause, BoundExpression value, LabelSymbol label, bool hasErrors = false) : base(BoundKind.SwitchExpressionArm, syntax, hasErrors || pattern.HasErrors() || whenClause.HasErrors() || value.HasErrors()) { @@ -4966,6 +4969,7 @@ public BoundSwitchExpressionArm(SyntaxNode syntax, ImmutableArray l this.Locals = locals; this.Pattern = pattern; + this.HasUnionMatching = hasUnionMatching; this.WhenClause = whenClause; this.Value = value; this.Label = label; @@ -4973,6 +4977,7 @@ public BoundSwitchExpressionArm(SyntaxNode syntax, ImmutableArray l public ImmutableArray Locals { get; } public BoundPattern Pattern { get; } + public bool HasUnionMatching { get; } public BoundExpression? WhenClause { get; } public BoundExpression Value { get; } public LabelSymbol Label { get; } @@ -4980,11 +4985,11 @@ public BoundSwitchExpressionArm(SyntaxNode syntax, ImmutableArray l [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitSwitchExpressionArm(this); - public BoundSwitchExpressionArm Update(ImmutableArray locals, BoundPattern pattern, BoundExpression? whenClause, BoundExpression value, LabelSymbol label) + public BoundSwitchExpressionArm Update(ImmutableArray locals, BoundPattern pattern, bool hasUnionMatching, BoundExpression? whenClause, BoundExpression value, LabelSymbol label) { - if (locals != this.Locals || pattern != this.Pattern || whenClause != this.WhenClause || value != this.Value || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(label, this.Label)) + if (locals != this.Locals || pattern != this.Pattern || hasUnionMatching != this.HasUnionMatching || whenClause != this.WhenClause || value != this.Value || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(label, this.Label)) { - var result = new BoundSwitchExpressionArm(this.Syntax, locals, pattern, whenClause, value, label, this.HasErrors); + var result = new BoundSwitchExpressionArm(this.Syntax, locals, pattern, hasUnionMatching, whenClause, value, label, this.HasErrors); result.CopyAttributes(this); return result; } @@ -5056,25 +5061,27 @@ public BoundConvertedSwitchExpression Update(TypeSymbol? naturalTypeOpt, bool wa internal sealed partial class BoundDecisionDag : BoundNode { - public BoundDecisionDag(SyntaxNode syntax, BoundDecisionDagNode rootNode, bool hasErrors = false) + public BoundDecisionDag(SyntaxNode syntax, BoundDecisionDagNode rootNode, bool suitableForLowering, bool hasErrors = false) : base(BoundKind.DecisionDag, syntax, hasErrors || rootNode.HasErrors()) { RoslynDebug.Assert(rootNode is object, "Field 'rootNode' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); this.RootNode = rootNode; + this.SuitableForLowering = suitableForLowering; } public BoundDecisionDagNode RootNode { get; } + public bool SuitableForLowering { get; } [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitDecisionDag(this); - public BoundDecisionDag Update(BoundDecisionDagNode rootNode) + public BoundDecisionDag Update(BoundDecisionDagNode rootNode, bool suitableForLowering) { - if (rootNode != this.RootNode) + if (rootNode != this.RootNode || suitableForLowering != this.SuitableForLowering) { - var result = new BoundDecisionDag(this.Syntax, rootNode, this.HasErrors); + var result = new BoundDecisionDag(this.Syntax, rootNode, suitableForLowering, this.HasErrors); result.CopyAttributes(this); return result; } @@ -5350,7 +5357,7 @@ public BoundDagExplicitNullTest(SyntaxNode syntax, BoundDagTemp input, bool hasE [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitDagExplicitNullTest(this); - public BoundDagExplicitNullTest Update(BoundDagTemp input) + public new BoundDagExplicitNullTest Update(BoundDagTemp input) { if (input != this.Input) { @@ -5708,6 +5715,32 @@ public BoundDagAssignmentEvaluation Update(BoundDagTemp target, BoundDagTemp inp } } + internal sealed partial class BoundDagPassThroughEvaluation : BoundDagEvaluation + { + public BoundDagPassThroughEvaluation(SyntaxNode syntax, BoundDagTemp input, bool hasErrors = false) + : base(BoundKind.DagPassThroughEvaluation, syntax, input, hasErrors || input.HasErrors()) + { + + RoslynDebug.Assert(input is object, "Field 'input' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); + + } + + + [DebuggerStepThrough] + public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitDagPassThroughEvaluation(this); + + public new BoundDagPassThroughEvaluation Update(BoundDagTemp input) + { + if (input != this.Input) + { + var result = new BoundDagPassThroughEvaluation(this.Syntax, input, this.HasErrors); + result.CopyAttributes(this); + return result; + } + return this; + } + } + internal sealed partial class BoundSwitchSection : BoundStatementList { public BoundSwitchSection(SyntaxNode syntax, ImmutableArray locals, ImmutableArray switchLabels, ImmutableArray statements, bool hasErrors = false) @@ -5742,7 +5775,7 @@ public BoundSwitchSection Update(ImmutableArray locals, ImmutableAr internal sealed partial class BoundSwitchLabel : BoundNode { - public BoundSwitchLabel(SyntaxNode syntax, LabelSymbol label, BoundPattern pattern, BoundExpression? whenClause, bool hasErrors = false) + public BoundSwitchLabel(SyntaxNode syntax, LabelSymbol label, BoundPattern pattern, bool hasUnionMatching, BoundExpression? whenClause, bool hasErrors = false) : base(BoundKind.SwitchLabel, syntax, hasErrors || pattern.HasErrors() || whenClause.HasErrors()) { @@ -5751,21 +5784,23 @@ public BoundSwitchLabel(SyntaxNode syntax, LabelSymbol label, BoundPattern patte this.Label = label; this.Pattern = pattern; + this.HasUnionMatching = hasUnionMatching; this.WhenClause = whenClause; } public LabelSymbol Label { get; } public BoundPattern Pattern { get; } + public bool HasUnionMatching { get; } public BoundExpression? WhenClause { get; } [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitSwitchLabel(this); - public BoundSwitchLabel Update(LabelSymbol label, BoundPattern pattern, BoundExpression? whenClause) + public BoundSwitchLabel Update(LabelSymbol label, BoundPattern pattern, bool hasUnionMatching, BoundExpression? whenClause) { - if (!Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(label, this.Label) || pattern != this.Pattern || whenClause != this.WhenClause) + if (!Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(label, this.Label) || pattern != this.Pattern || hasUnionMatching != this.HasUnionMatching || whenClause != this.WhenClause) { - var result = new BoundSwitchLabel(this.Syntax, label, pattern, whenClause, this.HasErrors); + var result = new BoundSwitchLabel(this.Syntax, label, pattern, hasUnionMatching, whenClause, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8049,7 +8084,7 @@ public BoundStringInsert Update(BoundExpression value, BoundExpression? alignmen internal sealed partial class BoundIsPatternExpression : BoundExpression { - public BoundIsPatternExpression(SyntaxNode syntax, BoundExpression expression, BoundPattern pattern, bool isNegated, BoundDecisionDag reachabilityDecisionDag, LabelSymbol whenTrueLabel, LabelSymbol whenFalseLabel, TypeSymbol? type, bool hasErrors = false) + public BoundIsPatternExpression(SyntaxNode syntax, BoundExpression expression, BoundPattern pattern, bool hasUnionMatching, bool isNegated, BoundDecisionDag reachabilityDecisionDag, LabelSymbol whenTrueLabel, LabelSymbol whenFalseLabel, TypeSymbol? type, bool hasErrors = false) : base(BoundKind.IsPatternExpression, syntax, type, hasErrors || expression.HasErrors() || pattern.HasErrors() || reachabilityDecisionDag.HasErrors()) { @@ -8061,6 +8096,7 @@ public BoundIsPatternExpression(SyntaxNode syntax, BoundExpression expression, B this.Expression = expression; this.Pattern = pattern; + this.HasUnionMatching = hasUnionMatching; this.IsNegated = isNegated; this.ReachabilityDecisionDag = reachabilityDecisionDag; this.WhenTrueLabel = whenTrueLabel; @@ -8069,6 +8105,7 @@ public BoundIsPatternExpression(SyntaxNode syntax, BoundExpression expression, B public BoundExpression Expression { get; } public BoundPattern Pattern { get; } + public bool HasUnionMatching { get; } public bool IsNegated { get; } public BoundDecisionDag ReachabilityDecisionDag { get; } public LabelSymbol WhenTrueLabel { get; } @@ -8077,11 +8114,11 @@ public BoundIsPatternExpression(SyntaxNode syntax, BoundExpression expression, B [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitIsPatternExpression(this); - public BoundIsPatternExpression Update(BoundExpression expression, BoundPattern pattern, bool isNegated, BoundDecisionDag reachabilityDecisionDag, LabelSymbol whenTrueLabel, LabelSymbol whenFalseLabel, TypeSymbol? type) + public BoundIsPatternExpression Update(BoundExpression expression, BoundPattern pattern, bool hasUnionMatching, bool isNegated, BoundDecisionDag reachabilityDecisionDag, LabelSymbol whenTrueLabel, LabelSymbol whenFalseLabel, TypeSymbol? type) { - if (expression != this.Expression || pattern != this.Pattern || isNegated != this.IsNegated || reachabilityDecisionDag != this.ReachabilityDecisionDag || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(whenTrueLabel, this.WhenTrueLabel) || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(whenFalseLabel, this.WhenFalseLabel) || !TypeSymbol.Equals(type, this.Type, TypeCompareKind.ConsiderEverything)) + if (expression != this.Expression || pattern != this.Pattern || hasUnionMatching != this.HasUnionMatching || isNegated != this.IsNegated || reachabilityDecisionDag != this.ReachabilityDecisionDag || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(whenTrueLabel, this.WhenTrueLabel) || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(whenFalseLabel, this.WhenFalseLabel) || !TypeSymbol.Equals(type, this.Type, TypeCompareKind.ConsiderEverything)) { - var result = new BoundIsPatternExpression(this.Syntax, expression, pattern, isNegated, reachabilityDecisionDag, whenTrueLabel, whenFalseLabel, type, this.HasErrors); + var result = new BoundIsPatternExpression(this.Syntax, expression, pattern, hasUnionMatching, isNegated, reachabilityDecisionDag, whenTrueLabel, whenFalseLabel, type, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8100,8 +8137,12 @@ protected BoundPattern(BoundKind kind, SyntaxNode syntax, TypeSymbol inputType, this.InputType = inputType; this.NarrowedType = narrowedType; + Validate(); } + [Conditional("DEBUG")] + private partial void Validate(); + protected BoundPattern(BoundKind kind, SyntaxNode syntax, TypeSymbol inputType, TypeSymbol narrowedType) : base(kind, syntax) { @@ -8119,7 +8160,7 @@ protected BoundPattern(BoundKind kind, SyntaxNode syntax, TypeSymbol inputType, internal sealed partial class BoundConstantPattern : BoundPattern { - public BoundConstantPattern(SyntaxNode syntax, BoundExpression value, ConstantValue constantValue, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + public BoundConstantPattern(SyntaxNode syntax, BoundExpression value, ConstantValue constantValue, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) : base(BoundKind.ConstantPattern, syntax, inputType, narrowedType, hasErrors || value.HasErrors()) { @@ -8130,6 +8171,7 @@ public BoundConstantPattern(SyntaxNode syntax, BoundExpression value, ConstantVa this.Value = value; this.ConstantValue = constantValue; + this.IsUnionMatching = isUnionMatching; Validate(); } @@ -8138,15 +8180,58 @@ public BoundConstantPattern(SyntaxNode syntax, BoundExpression value, ConstantVa public BoundExpression Value { get; } public ConstantValue ConstantValue { get; } + public override bool IsUnionMatching { get; } [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitConstantPattern(this); - public BoundConstantPattern Update(BoundExpression value, ConstantValue constantValue, TypeSymbol inputType, TypeSymbol narrowedType) + public BoundConstantPattern Update(BoundExpression value, ConstantValue constantValue, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType) + { + if (value != this.Value || constantValue != this.ConstantValue || isUnionMatching != this.IsUnionMatching || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) + { + var result = new BoundConstantPattern(this.Syntax, value, constantValue, isUnionMatching, inputType, narrowedType, this.HasErrors); + result.CopyAttributes(this); + return result; + } + return this; + } + } + + internal sealed partial class BoundPatternWithUnionMatching : BoundPattern + { + public BoundPatternWithUnionMatching(SyntaxNode syntax, TypeSymbol unionMatchingInputType, BoundPattern? leftOfPendingConjunction, BoundPropertySubpatternMember valueProperty, BoundPattern valuePattern, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + : base(BoundKind.PatternWithUnionMatching, syntax, inputType, narrowedType, hasErrors || leftOfPendingConjunction.HasErrors() || valueProperty.HasErrors() || valuePattern.HasErrors()) { - if (value != this.Value || constantValue != this.ConstantValue || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) + + RoslynDebug.Assert(unionMatchingInputType is object, "Field 'unionMatchingInputType' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); + RoslynDebug.Assert(valueProperty is object, "Field 'valueProperty' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); + RoslynDebug.Assert(valuePattern is object, "Field 'valuePattern' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); + RoslynDebug.Assert(inputType is object, "Field 'inputType' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); + RoslynDebug.Assert(narrowedType is object, "Field 'narrowedType' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); + + this.UnionMatchingInputType = unionMatchingInputType; + this.LeftOfPendingConjunction = leftOfPendingConjunction; + this.ValueProperty = valueProperty; + this.ValuePattern = valuePattern; + Validate(); + } + + [Conditional("DEBUG")] + private partial void Validate(); + + public TypeSymbol UnionMatchingInputType { get; } + public BoundPattern? LeftOfPendingConjunction { get; } + public BoundPropertySubpatternMember ValueProperty { get; } + public BoundPattern ValuePattern { get; } + + [DebuggerStepThrough] + public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitPatternWithUnionMatching(this); + + public BoundPatternWithUnionMatching Update(TypeSymbol unionMatchingInputType, BoundPattern? leftOfPendingConjunction, BoundPropertySubpatternMember valueProperty, BoundPattern valuePattern, TypeSymbol inputType, TypeSymbol narrowedType) + { + if (!TypeSymbol.Equals(unionMatchingInputType, this.UnionMatchingInputType, TypeCompareKind.ConsiderEverything) || leftOfPendingConjunction != this.LeftOfPendingConjunction || valueProperty != this.ValueProperty || valuePattern != this.ValuePattern || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) { - var result = new BoundConstantPattern(this.Syntax, value, constantValue, inputType, narrowedType, this.HasErrors); + var result = new BoundPatternWithUnionMatching(this.Syntax, unionMatchingInputType, leftOfPendingConjunction, valueProperty, valuePattern, inputType, narrowedType, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8196,7 +8281,7 @@ public BoundDiscardPattern Update(TypeSymbol inputType, TypeSymbol narrowedType) internal abstract partial class BoundObjectPattern : BoundPattern { - protected BoundObjectPattern(BoundKind kind, SyntaxNode syntax, Symbol? variable, BoundExpression? variableAccess, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + protected BoundObjectPattern(BoundKind kind, SyntaxNode syntax, Symbol? variable, BoundExpression? variableAccess, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) : base(kind, syntax, inputType, narrowedType, hasErrors) { @@ -8205,16 +8290,18 @@ protected BoundObjectPattern(BoundKind kind, SyntaxNode syntax, Symbol? variable this.Variable = variable; this.VariableAccess = variableAccess; + this.IsUnionMatching = isUnionMatching; } public Symbol? Variable { get; } public BoundExpression? VariableAccess { get; } + public override bool IsUnionMatching { get; } } internal sealed partial class BoundDeclarationPattern : BoundObjectPattern { - public BoundDeclarationPattern(SyntaxNode syntax, BoundTypeExpression declaredType, bool isVar, Symbol? variable, BoundExpression? variableAccess, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) - : base(BoundKind.DeclarationPattern, syntax, variable, variableAccess, inputType, narrowedType, hasErrors || declaredType.HasErrors() || variableAccess.HasErrors()) + public BoundDeclarationPattern(SyntaxNode syntax, BoundTypeExpression declaredType, bool isVar, Symbol? variable, BoundExpression? variableAccess, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + : base(BoundKind.DeclarationPattern, syntax, variable, variableAccess, isUnionMatching, inputType, narrowedType, hasErrors || declaredType.HasErrors() || variableAccess.HasErrors()) { RoslynDebug.Assert(declaredType is object, "Field 'declaredType' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); @@ -8235,11 +8322,11 @@ public BoundDeclarationPattern(SyntaxNode syntax, BoundTypeExpression declaredTy [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitDeclarationPattern(this); - public BoundDeclarationPattern Update(BoundTypeExpression declaredType, bool isVar, Symbol? variable, BoundExpression? variableAccess, TypeSymbol inputType, TypeSymbol narrowedType) + public BoundDeclarationPattern Update(BoundTypeExpression declaredType, bool isVar, Symbol? variable, BoundExpression? variableAccess, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType) { - if (declaredType != this.DeclaredType || isVar != this.IsVar || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(variable, this.Variable) || variableAccess != this.VariableAccess || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) + if (declaredType != this.DeclaredType || isVar != this.IsVar || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(variable, this.Variable) || variableAccess != this.VariableAccess || isUnionMatching != this.IsUnionMatching || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) { - var result = new BoundDeclarationPattern(this.Syntax, declaredType, isVar, variable, variableAccess, inputType, narrowedType, this.HasErrors); + var result = new BoundDeclarationPattern(this.Syntax, declaredType, isVar, variable, variableAccess, isUnionMatching, inputType, narrowedType, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8249,8 +8336,8 @@ public BoundDeclarationPattern Update(BoundTypeExpression declaredType, bool isV internal sealed partial class BoundRecursivePattern : BoundObjectPattern { - public BoundRecursivePattern(SyntaxNode syntax, BoundTypeExpression? declaredType, MethodSymbol? deconstructMethod, ImmutableArray deconstruction, ImmutableArray properties, bool isExplicitNotNullTest, Symbol? variable, BoundExpression? variableAccess, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) - : base(BoundKind.RecursivePattern, syntax, variable, variableAccess, inputType, narrowedType, hasErrors || declaredType.HasErrors() || deconstruction.HasErrors() || properties.HasErrors() || variableAccess.HasErrors()) + public BoundRecursivePattern(SyntaxNode syntax, BoundTypeExpression? declaredType, MethodSymbol? deconstructMethod, ImmutableArray deconstruction, ImmutableArray properties, bool isExplicitNotNullTest, Symbol? variable, BoundExpression? variableAccess, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + : base(BoundKind.RecursivePattern, syntax, variable, variableAccess, isUnionMatching, inputType, narrowedType, hasErrors || declaredType.HasErrors() || deconstruction.HasErrors() || properties.HasErrors() || variableAccess.HasErrors()) { RoslynDebug.Assert(inputType is object, "Field 'inputType' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); @@ -8276,11 +8363,11 @@ public BoundRecursivePattern(SyntaxNode syntax, BoundTypeExpression? declaredTyp [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitRecursivePattern(this); - public BoundRecursivePattern Update(BoundTypeExpression? declaredType, MethodSymbol? deconstructMethod, ImmutableArray deconstruction, ImmutableArray properties, bool isExplicitNotNullTest, Symbol? variable, BoundExpression? variableAccess, TypeSymbol inputType, TypeSymbol narrowedType) + public BoundRecursivePattern Update(BoundTypeExpression? declaredType, MethodSymbol? deconstructMethod, ImmutableArray deconstruction, ImmutableArray properties, bool isExplicitNotNullTest, Symbol? variable, BoundExpression? variableAccess, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType) { - if (declaredType != this.DeclaredType || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(deconstructMethod, this.DeconstructMethod) || deconstruction != this.Deconstruction || properties != this.Properties || isExplicitNotNullTest != this.IsExplicitNotNullTest || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(variable, this.Variable) || variableAccess != this.VariableAccess || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) + if (declaredType != this.DeclaredType || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(deconstructMethod, this.DeconstructMethod) || deconstruction != this.Deconstruction || properties != this.Properties || isExplicitNotNullTest != this.IsExplicitNotNullTest || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(variable, this.Variable) || variableAccess != this.VariableAccess || isUnionMatching != this.IsUnionMatching || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) { - var result = new BoundRecursivePattern(this.Syntax, declaredType, deconstructMethod, deconstruction, properties, isExplicitNotNullTest, variable, variableAccess, inputType, narrowedType, this.HasErrors); + var result = new BoundRecursivePattern(this.Syntax, declaredType, deconstructMethod, deconstruction, properties, isExplicitNotNullTest, variable, variableAccess, isUnionMatching, inputType, narrowedType, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8290,8 +8377,8 @@ public BoundRecursivePattern Update(BoundTypeExpression? declaredType, MethodSym internal sealed partial class BoundListPattern : BoundObjectPattern { - public BoundListPattern(SyntaxNode syntax, ImmutableArray subpatterns, bool hasSlice, BoundExpression? lengthAccess, BoundExpression? indexerAccess, BoundListPatternReceiverPlaceholder? receiverPlaceholder, BoundListPatternIndexPlaceholder? argumentPlaceholder, Symbol? variable, BoundExpression? variableAccess, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) - : base(BoundKind.ListPattern, syntax, variable, variableAccess, inputType, narrowedType, hasErrors || subpatterns.HasErrors() || lengthAccess.HasErrors() || indexerAccess.HasErrors() || receiverPlaceholder.HasErrors() || argumentPlaceholder.HasErrors() || variableAccess.HasErrors()) + public BoundListPattern(SyntaxNode syntax, ImmutableArray subpatterns, bool hasSlice, BoundExpression? lengthAccess, BoundExpression? indexerAccess, BoundListPatternReceiverPlaceholder? receiverPlaceholder, BoundListPatternIndexPlaceholder? argumentPlaceholder, Symbol? variable, BoundExpression? variableAccess, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + : base(BoundKind.ListPattern, syntax, variable, variableAccess, isUnionMatching, inputType, narrowedType, hasErrors || subpatterns.HasErrors() || lengthAccess.HasErrors() || indexerAccess.HasErrors() || receiverPlaceholder.HasErrors() || argumentPlaceholder.HasErrors() || variableAccess.HasErrors()) { RoslynDebug.Assert(!subpatterns.IsDefault, "Field 'subpatterns' cannot be null (use Null=\"allow\" in BoundNodes.xml to remove this check)"); @@ -8320,11 +8407,11 @@ public BoundListPattern(SyntaxNode syntax, ImmutableArray subpatte [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitListPattern(this); - public BoundListPattern Update(ImmutableArray subpatterns, bool hasSlice, BoundExpression? lengthAccess, BoundExpression? indexerAccess, BoundListPatternReceiverPlaceholder? receiverPlaceholder, BoundListPatternIndexPlaceholder? argumentPlaceholder, Symbol? variable, BoundExpression? variableAccess, TypeSymbol inputType, TypeSymbol narrowedType) + public BoundListPattern Update(ImmutableArray subpatterns, bool hasSlice, BoundExpression? lengthAccess, BoundExpression? indexerAccess, BoundListPatternReceiverPlaceholder? receiverPlaceholder, BoundListPatternIndexPlaceholder? argumentPlaceholder, Symbol? variable, BoundExpression? variableAccess, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType) { - if (subpatterns != this.Subpatterns || hasSlice != this.HasSlice || lengthAccess != this.LengthAccess || indexerAccess != this.IndexerAccess || receiverPlaceholder != this.ReceiverPlaceholder || argumentPlaceholder != this.ArgumentPlaceholder || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(variable, this.Variable) || variableAccess != this.VariableAccess || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) + if (subpatterns != this.Subpatterns || hasSlice != this.HasSlice || lengthAccess != this.LengthAccess || indexerAccess != this.IndexerAccess || receiverPlaceholder != this.ReceiverPlaceholder || argumentPlaceholder != this.ArgumentPlaceholder || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(variable, this.Variable) || variableAccess != this.VariableAccess || isUnionMatching != this.IsUnionMatching || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) { - var result = new BoundListPattern(this.Syntax, subpatterns, hasSlice, lengthAccess, indexerAccess, receiverPlaceholder, argumentPlaceholder, variable, variableAccess, inputType, narrowedType, this.HasErrors); + var result = new BoundListPattern(this.Syntax, subpatterns, hasSlice, lengthAccess, indexerAccess, receiverPlaceholder, argumentPlaceholder, variable, variableAccess, isUnionMatching, inputType, narrowedType, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8373,7 +8460,7 @@ public BoundSlicePattern Update(BoundPattern? pattern, BoundExpression? indexerA internal sealed partial class BoundITuplePattern : BoundPattern { - public BoundITuplePattern(SyntaxNode syntax, MethodSymbol getLengthMethod, MethodSymbol getItemMethod, ImmutableArray subpatterns, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + public BoundITuplePattern(SyntaxNode syntax, MethodSymbol getLengthMethod, MethodSymbol getItemMethod, ImmutableArray subpatterns, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) : base(BoundKind.ITuplePattern, syntax, inputType, narrowedType, hasErrors || subpatterns.HasErrors()) { @@ -8386,6 +8473,7 @@ public BoundITuplePattern(SyntaxNode syntax, MethodSymbol getLengthMethod, Metho this.GetLengthMethod = getLengthMethod; this.GetItemMethod = getItemMethod; this.Subpatterns = subpatterns; + this.IsUnionMatching = isUnionMatching; Validate(); } @@ -8395,15 +8483,16 @@ public BoundITuplePattern(SyntaxNode syntax, MethodSymbol getLengthMethod, Metho public MethodSymbol GetLengthMethod { get; } public MethodSymbol GetItemMethod { get; } public ImmutableArray Subpatterns { get; } + public override bool IsUnionMatching { get; } [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitITuplePattern(this); - public BoundITuplePattern Update(MethodSymbol getLengthMethod, MethodSymbol getItemMethod, ImmutableArray subpatterns, TypeSymbol inputType, TypeSymbol narrowedType) + public BoundITuplePattern Update(MethodSymbol getLengthMethod, MethodSymbol getItemMethod, ImmutableArray subpatterns, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType) { - if (!Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(getLengthMethod, this.GetLengthMethod) || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(getItemMethod, this.GetItemMethod) || subpatterns != this.Subpatterns || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) + if (!Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(getLengthMethod, this.GetLengthMethod) || !Symbols.SymbolEqualityComparer.ConsiderEverything.Equals(getItemMethod, this.GetItemMethod) || subpatterns != this.Subpatterns || isUnionMatching != this.IsUnionMatching || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) { - var result = new BoundITuplePattern(this.Syntax, getLengthMethod, getItemMethod, subpatterns, inputType, narrowedType, this.HasErrors); + var result = new BoundITuplePattern(this.Syntax, getLengthMethod, getItemMethod, subpatterns, isUnionMatching, inputType, narrowedType, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8420,8 +8509,12 @@ protected BoundSubpattern(BoundKind kind, SyntaxNode syntax, BoundPattern patter RoslynDebug.Assert(pattern is object, "Field 'pattern' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); this.Pattern = pattern; + Validate(); } + [Conditional("DEBUG")] + private partial void Validate(); + public BoundPattern Pattern { get; } } @@ -8517,7 +8610,7 @@ public BoundPropertySubpatternMember Update(BoundPropertySubpatternMember? recei internal sealed partial class BoundTypePattern : BoundPattern { - public BoundTypePattern(SyntaxNode syntax, BoundTypeExpression declaredType, bool isExplicitNotNullTest, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + public BoundTypePattern(SyntaxNode syntax, BoundTypeExpression declaredType, bool isExplicitNotNullTest, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) : base(BoundKind.TypePattern, syntax, inputType, narrowedType, hasErrors || declaredType.HasErrors()) { @@ -8527,6 +8620,7 @@ public BoundTypePattern(SyntaxNode syntax, BoundTypeExpression declaredType, boo this.DeclaredType = declaredType; this.IsExplicitNotNullTest = isExplicitNotNullTest; + this.IsUnionMatching = isUnionMatching; Validate(); } @@ -8535,15 +8629,16 @@ public BoundTypePattern(SyntaxNode syntax, BoundTypeExpression declaredType, boo public BoundTypeExpression DeclaredType { get; } public bool IsExplicitNotNullTest { get; } + public override bool IsUnionMatching { get; } [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitTypePattern(this); - public BoundTypePattern Update(BoundTypeExpression declaredType, bool isExplicitNotNullTest, TypeSymbol inputType, TypeSymbol narrowedType) + public BoundTypePattern Update(BoundTypeExpression declaredType, bool isExplicitNotNullTest, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType) { - if (declaredType != this.DeclaredType || isExplicitNotNullTest != this.IsExplicitNotNullTest || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) + if (declaredType != this.DeclaredType || isExplicitNotNullTest != this.IsExplicitNotNullTest || isUnionMatching != this.IsUnionMatching || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) { - var result = new BoundTypePattern(this.Syntax, declaredType, isExplicitNotNullTest, inputType, narrowedType, this.HasErrors); + var result = new BoundTypePattern(this.Syntax, declaredType, isExplicitNotNullTest, isUnionMatching, inputType, narrowedType, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8592,7 +8687,7 @@ public BoundBinaryPattern Update(bool disjunction, BoundPattern left, BoundPatte internal sealed partial class BoundNegatedPattern : BoundPattern { - public BoundNegatedPattern(SyntaxNode syntax, BoundPattern negated, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + public BoundNegatedPattern(SyntaxNode syntax, BoundPattern negated, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) : base(BoundKind.NegatedPattern, syntax, inputType, narrowedType, hasErrors || negated.HasErrors()) { @@ -8601,6 +8696,7 @@ public BoundNegatedPattern(SyntaxNode syntax, BoundPattern negated, TypeSymbol i RoslynDebug.Assert(narrowedType is object, "Field 'narrowedType' cannot be null (make the type nullable in BoundNodes.xml to remove this check)"); this.Negated = negated; + this.IsUnionMatching = isUnionMatching; Validate(); } @@ -8608,15 +8704,16 @@ public BoundNegatedPattern(SyntaxNode syntax, BoundPattern negated, TypeSymbol i private partial void Validate(); public BoundPattern Negated { get; } + public override bool IsUnionMatching { get; } [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitNegatedPattern(this); - public BoundNegatedPattern Update(BoundPattern negated, TypeSymbol inputType, TypeSymbol narrowedType) + public BoundNegatedPattern Update(BoundPattern negated, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType) { - if (negated != this.Negated || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) + if (negated != this.Negated || isUnionMatching != this.IsUnionMatching || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) { - var result = new BoundNegatedPattern(this.Syntax, negated, inputType, narrowedType, this.HasErrors); + var result = new BoundNegatedPattern(this.Syntax, negated, isUnionMatching, inputType, narrowedType, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8626,7 +8723,7 @@ public BoundNegatedPattern Update(BoundPattern negated, TypeSymbol inputType, Ty internal sealed partial class BoundRelationalPattern : BoundPattern { - public BoundRelationalPattern(SyntaxNode syntax, BinaryOperatorKind relation, BoundExpression value, ConstantValue constantValue, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) + public BoundRelationalPattern(SyntaxNode syntax, BinaryOperatorKind relation, BoundExpression value, ConstantValue constantValue, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType, bool hasErrors = false) : base(BoundKind.RelationalPattern, syntax, inputType, narrowedType, hasErrors || value.HasErrors()) { @@ -8638,6 +8735,7 @@ public BoundRelationalPattern(SyntaxNode syntax, BinaryOperatorKind relation, Bo this.Relation = relation; this.Value = value; this.ConstantValue = constantValue; + this.IsUnionMatching = isUnionMatching; Validate(); } @@ -8647,15 +8745,16 @@ public BoundRelationalPattern(SyntaxNode syntax, BinaryOperatorKind relation, Bo public BinaryOperatorKind Relation { get; } public BoundExpression Value { get; } public ConstantValue ConstantValue { get; } + public override bool IsUnionMatching { get; } [DebuggerStepThrough] public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitRelationalPattern(this); - public BoundRelationalPattern Update(BinaryOperatorKind relation, BoundExpression value, ConstantValue constantValue, TypeSymbol inputType, TypeSymbol narrowedType) + public BoundRelationalPattern Update(BinaryOperatorKind relation, BoundExpression value, ConstantValue constantValue, bool isUnionMatching, TypeSymbol inputType, TypeSymbol narrowedType) { - if (relation != this.Relation || value != this.Value || constantValue != this.ConstantValue || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) + if (relation != this.Relation || value != this.Value || constantValue != this.ConstantValue || isUnionMatching != this.IsUnionMatching || !TypeSymbol.Equals(inputType, this.InputType, TypeCompareKind.ConsiderEverything) || !TypeSymbol.Equals(narrowedType, this.NarrowedType, TypeCompareKind.ConsiderEverything)) { - var result = new BoundRelationalPattern(this.Syntax, relation, value, constantValue, inputType, narrowedType, this.HasErrors); + var result = new BoundRelationalPattern(this.Syntax, relation, value, constantValue, isUnionMatching, inputType, narrowedType, this.HasErrors); result.CopyAttributes(this); return result; } @@ -8927,6 +9026,31 @@ public BoundExpressionWithNullability Update(BoundExpression expression, Nullabl } } + internal sealed partial class BoundValueForNullableAnalysis : BoundExpression + { + public BoundValueForNullableAnalysis(SyntaxNode syntax, BoundExpression? originalExpression, TypeSymbol? type, bool hasErrors = false) + : base(BoundKind.ValueForNullableAnalysis, syntax, type, hasErrors || originalExpression.HasErrors()) + { + this.OriginalExpression = originalExpression; + } + + public BoundExpression? OriginalExpression { get; } + + [DebuggerStepThrough] + public override BoundNode? Accept(BoundTreeVisitor visitor) => visitor.VisitValueForNullableAnalysis(this); + + public BoundValueForNullableAnalysis Update(BoundExpression? originalExpression, TypeSymbol? type) + { + if (originalExpression != this.OriginalExpression || !TypeSymbol.Equals(type, this.Type, TypeCompareKind.ConsiderEverything)) + { + var result = new BoundValueForNullableAnalysis(this.Syntax, originalExpression, type, this.HasErrors); + result.CopyAttributes(this); + return result; + } + return this; + } + } + internal sealed partial class BoundWithExpression : BoundExpression { public BoundWithExpression(SyntaxNode syntax, BoundExpression receiver, MethodSymbol? cloneMethod, BoundObjectInitializerExpressionBase initializerExpression, TypeSymbol type, bool hasErrors = false) @@ -9270,6 +9394,8 @@ internal R VisitInternal(BoundNode node, A arg) return VisitDagSliceEvaluation((BoundDagSliceEvaluation)node, arg); case BoundKind.DagAssignmentEvaluation: return VisitDagAssignmentEvaluation((BoundDagAssignmentEvaluation)node, arg); + case BoundKind.DagPassThroughEvaluation: + return VisitDagPassThroughEvaluation((BoundDagPassThroughEvaluation)node, arg); case BoundKind.SwitchSection: return VisitSwitchSection((BoundSwitchSection)node, arg); case BoundKind.SwitchLabel: @@ -9394,6 +9520,8 @@ internal R VisitInternal(BoundNode node, A arg) return VisitIsPatternExpression((BoundIsPatternExpression)node, arg); case BoundKind.ConstantPattern: return VisitConstantPattern((BoundConstantPattern)node, arg); + case BoundKind.PatternWithUnionMatching: + return VisitPatternWithUnionMatching((BoundPatternWithUnionMatching)node, arg); case BoundKind.DiscardPattern: return VisitDiscardPattern((BoundDiscardPattern)node, arg); case BoundKind.DeclarationPattern: @@ -9436,6 +9564,8 @@ internal R VisitInternal(BoundNode node, A arg) return VisitConstructorMethodBody((BoundConstructorMethodBody)node, arg); case BoundKind.ExpressionWithNullability: return VisitExpressionWithNullability((BoundExpressionWithNullability)node, arg); + case BoundKind.ValueForNullableAnalysis: + return VisitValueForNullableAnalysis((BoundValueForNullableAnalysis)node, arg); case BoundKind.WithExpression: return VisitWithExpression((BoundWithExpression)node, arg); } @@ -9596,6 +9726,7 @@ internal abstract partial class BoundTreeVisitor public virtual R VisitDagIndexerEvaluation(BoundDagIndexerEvaluation node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitDagSliceEvaluation(BoundDagSliceEvaluation node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitDagAssignmentEvaluation(BoundDagAssignmentEvaluation node, A arg) => this.DefaultVisit(node, arg); + public virtual R VisitDagPassThroughEvaluation(BoundDagPassThroughEvaluation node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitSwitchSection(BoundSwitchSection node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitSwitchLabel(BoundSwitchLabel node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitSequencePointExpression(BoundSequencePointExpression node, A arg) => this.DefaultVisit(node, arg); @@ -9658,6 +9789,7 @@ internal abstract partial class BoundTreeVisitor public virtual R VisitStringInsert(BoundStringInsert node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitIsPatternExpression(BoundIsPatternExpression node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitConstantPattern(BoundConstantPattern node, A arg) => this.DefaultVisit(node, arg); + public virtual R VisitPatternWithUnionMatching(BoundPatternWithUnionMatching node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitDiscardPattern(BoundDiscardPattern node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitDeclarationPattern(BoundDeclarationPattern node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitRecursivePattern(BoundRecursivePattern node, A arg) => this.DefaultVisit(node, arg); @@ -9679,6 +9811,7 @@ internal abstract partial class BoundTreeVisitor public virtual R VisitNonConstructorMethodBody(BoundNonConstructorMethodBody node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitConstructorMethodBody(BoundConstructorMethodBody node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitExpressionWithNullability(BoundExpressionWithNullability node, A arg) => this.DefaultVisit(node, arg); + public virtual R VisitValueForNullableAnalysis(BoundValueForNullableAnalysis node, A arg) => this.DefaultVisit(node, arg); public virtual R VisitWithExpression(BoundWithExpression node, A arg) => this.DefaultVisit(node, arg); } @@ -9834,6 +9967,7 @@ internal abstract partial class BoundTreeVisitor public virtual BoundNode? VisitDagIndexerEvaluation(BoundDagIndexerEvaluation node) => this.DefaultVisit(node); public virtual BoundNode? VisitDagSliceEvaluation(BoundDagSliceEvaluation node) => this.DefaultVisit(node); public virtual BoundNode? VisitDagAssignmentEvaluation(BoundDagAssignmentEvaluation node) => this.DefaultVisit(node); + public virtual BoundNode? VisitDagPassThroughEvaluation(BoundDagPassThroughEvaluation node) => this.DefaultVisit(node); public virtual BoundNode? VisitSwitchSection(BoundSwitchSection node) => this.DefaultVisit(node); public virtual BoundNode? VisitSwitchLabel(BoundSwitchLabel node) => this.DefaultVisit(node); public virtual BoundNode? VisitSequencePointExpression(BoundSequencePointExpression node) => this.DefaultVisit(node); @@ -9896,6 +10030,7 @@ internal abstract partial class BoundTreeVisitor public virtual BoundNode? VisitStringInsert(BoundStringInsert node) => this.DefaultVisit(node); public virtual BoundNode? VisitIsPatternExpression(BoundIsPatternExpression node) => this.DefaultVisit(node); public virtual BoundNode? VisitConstantPattern(BoundConstantPattern node) => this.DefaultVisit(node); + public virtual BoundNode? VisitPatternWithUnionMatching(BoundPatternWithUnionMatching node) => this.DefaultVisit(node); public virtual BoundNode? VisitDiscardPattern(BoundDiscardPattern node) => this.DefaultVisit(node); public virtual BoundNode? VisitDeclarationPattern(BoundDeclarationPattern node) => this.DefaultVisit(node); public virtual BoundNode? VisitRecursivePattern(BoundRecursivePattern node) => this.DefaultVisit(node); @@ -9917,6 +10052,7 @@ internal abstract partial class BoundTreeVisitor public virtual BoundNode? VisitNonConstructorMethodBody(BoundNonConstructorMethodBody node) => this.DefaultVisit(node); public virtual BoundNode? VisitConstructorMethodBody(BoundConstructorMethodBody node) => this.DefaultVisit(node); public virtual BoundNode? VisitExpressionWithNullability(BoundExpressionWithNullability node) => this.DefaultVisit(node); + public virtual BoundNode? VisitValueForNullableAnalysis(BoundValueForNullableAnalysis node) => this.DefaultVisit(node); public virtual BoundNode? VisitWithExpression(BoundWithExpression node) => this.DefaultVisit(node); } @@ -10536,6 +10672,11 @@ internal abstract partial class BoundTreeWalker : BoundTreeVisitor this.Visit(node.Input); return null; } + public override BoundNode? VisitDagPassThroughEvaluation(BoundDagPassThroughEvaluation node) + { + this.Visit(node.Input); + return null; + } public override BoundNode? VisitSwitchSection(BoundSwitchSection node) { this.VisitList(node.SwitchLabels); @@ -10841,6 +10982,13 @@ internal abstract partial class BoundTreeWalker : BoundTreeVisitor this.Visit(node.Value); return null; } + public override BoundNode? VisitPatternWithUnionMatching(BoundPatternWithUnionMatching node) + { + this.Visit(node.LeftOfPendingConjunction); + this.Visit(node.ValueProperty); + this.Visit(node.ValuePattern); + return null; + } public override BoundNode? VisitDiscardPattern(BoundDiscardPattern node) => null; public override BoundNode? VisitDeclarationPattern(BoundDeclarationPattern node) { @@ -10944,6 +11092,11 @@ internal abstract partial class BoundTreeWalker : BoundTreeVisitor this.Visit(node.Expression); return null; } + public override BoundNode? VisitValueForNullableAnalysis(BoundValueForNullableAnalysis node) + { + this.Visit(node.OriginalExpression); + return null; + } public override BoundNode? VisitWithExpression(BoundWithExpression node) { this.Visit(node.Receiver); @@ -11776,7 +11929,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor BoundPattern pattern = (BoundPattern)this.Visit(node.Pattern); BoundExpression? whenClause = (BoundExpression?)this.Visit(node.WhenClause); BoundExpression value = (BoundExpression)this.Visit(node.Value); - return node.Update(locals, pattern, whenClause, value, label); + return node.Update(locals, pattern, node.HasUnionMatching, whenClause, value, label); } public override BoundNode? VisitUnconvertedSwitchExpression(BoundUnconvertedSwitchExpression node) { @@ -11800,7 +11953,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor public override BoundNode? VisitDecisionDag(BoundDecisionDag node) { BoundDecisionDagNode rootNode = (BoundDecisionDagNode)this.Visit(node.RootNode); - return node.Update(rootNode); + return node.Update(rootNode, node.SuitableForLowering); } public override BoundNode? VisitEvaluationDecisionDagNode(BoundEvaluationDecisionDagNode node) { @@ -11915,6 +12068,11 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor BoundDagTemp input = (BoundDagTemp)this.Visit(node.Input); return node.Update(target, input); } + public override BoundNode? VisitDagPassThroughEvaluation(BoundDagPassThroughEvaluation node) + { + BoundDagTemp input = (BoundDagTemp)this.Visit(node.Input); + return node.Update(input); + } public override BoundNode? VisitSwitchSection(BoundSwitchSection node) { ImmutableArray locals = this.VisitLocals(node.Locals); @@ -11927,7 +12085,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor LabelSymbol label = this.VisitLabelSymbol(node.Label); BoundPattern pattern = (BoundPattern)this.Visit(node.Pattern); BoundExpression? whenClause = (BoundExpression?)this.Visit(node.WhenClause); - return node.Update(label, pattern, whenClause); + return node.Update(label, pattern, node.HasUnionMatching, whenClause); } public override BoundNode? VisitSequencePointExpression(BoundSequencePointExpression node) { @@ -12354,14 +12512,24 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor BoundPattern pattern = (BoundPattern)this.Visit(node.Pattern); BoundDecisionDag reachabilityDecisionDag = node.ReachabilityDecisionDag; TypeSymbol? type = this.VisitType(node.Type); - return node.Update(expression, pattern, node.IsNegated, reachabilityDecisionDag, whenTrueLabel, whenFalseLabel, type); + return node.Update(expression, pattern, node.HasUnionMatching, node.IsNegated, reachabilityDecisionDag, whenTrueLabel, whenFalseLabel, type); } public override BoundNode? VisitConstantPattern(BoundConstantPattern node) { BoundExpression value = (BoundExpression)this.Visit(node.Value); TypeSymbol? inputType = this.VisitType(node.InputType); TypeSymbol? narrowedType = this.VisitType(node.NarrowedType); - return node.Update(value, node.ConstantValue, inputType, narrowedType); + return node.Update(value, node.ConstantValue, node.IsUnionMatching, inputType, narrowedType); + } + public override BoundNode? VisitPatternWithUnionMatching(BoundPatternWithUnionMatching node) + { + BoundPattern? leftOfPendingConjunction = (BoundPattern?)this.Visit(node.LeftOfPendingConjunction); + BoundPropertySubpatternMember valueProperty = (BoundPropertySubpatternMember)this.Visit(node.ValueProperty); + BoundPattern valuePattern = (BoundPattern)this.Visit(node.ValuePattern); + TypeSymbol? unionMatchingInputType = this.VisitType(node.UnionMatchingInputType); + TypeSymbol? inputType = this.VisitType(node.InputType); + TypeSymbol? narrowedType = this.VisitType(node.NarrowedType); + return node.Update(unionMatchingInputType, leftOfPendingConjunction, valueProperty, valuePattern, inputType, narrowedType); } public override BoundNode? VisitDiscardPattern(BoundDiscardPattern node) { @@ -12376,7 +12544,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor BoundExpression? variableAccess = (BoundExpression?)this.Visit(node.VariableAccess); TypeSymbol? inputType = this.VisitType(node.InputType); TypeSymbol? narrowedType = this.VisitType(node.NarrowedType); - return node.Update(declaredType, node.IsVar, variable, variableAccess, inputType, narrowedType); + return node.Update(declaredType, node.IsVar, variable, variableAccess, node.IsUnionMatching, inputType, narrowedType); } public override BoundNode? VisitRecursivePattern(BoundRecursivePattern node) { @@ -12388,7 +12556,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor BoundExpression? variableAccess = (BoundExpression?)this.Visit(node.VariableAccess); TypeSymbol? inputType = this.VisitType(node.InputType); TypeSymbol? narrowedType = this.VisitType(node.NarrowedType); - return node.Update(declaredType, deconstructMethod, deconstruction, properties, node.IsExplicitNotNullTest, variable, variableAccess, inputType, narrowedType); + return node.Update(declaredType, deconstructMethod, deconstruction, properties, node.IsExplicitNotNullTest, variable, variableAccess, node.IsUnionMatching, inputType, narrowedType); } public override BoundNode? VisitListPattern(BoundListPattern node) { @@ -12401,7 +12569,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor BoundExpression? variableAccess = (BoundExpression?)this.Visit(node.VariableAccess); TypeSymbol? inputType = this.VisitType(node.InputType); TypeSymbol? narrowedType = this.VisitType(node.NarrowedType); - return node.Update(subpatterns, node.HasSlice, lengthAccess, indexerAccess, receiverPlaceholder, argumentPlaceholder, variable, variableAccess, inputType, narrowedType); + return node.Update(subpatterns, node.HasSlice, lengthAccess, indexerAccess, receiverPlaceholder, argumentPlaceholder, variable, variableAccess, node.IsUnionMatching, inputType, narrowedType); } public override BoundNode? VisitSlicePattern(BoundSlicePattern node) { @@ -12420,7 +12588,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor ImmutableArray subpatterns = this.VisitList(node.Subpatterns); TypeSymbol? inputType = this.VisitType(node.InputType); TypeSymbol? narrowedType = this.VisitType(node.NarrowedType); - return node.Update(getLengthMethod, getItemMethod, subpatterns, inputType, narrowedType); + return node.Update(getLengthMethod, getItemMethod, subpatterns, node.IsUnionMatching, inputType, narrowedType); } public override BoundNode? VisitPositionalSubpattern(BoundPositionalSubpattern node) { @@ -12446,7 +12614,7 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor BoundTypeExpression declaredType = (BoundTypeExpression)this.Visit(node.DeclaredType); TypeSymbol? inputType = this.VisitType(node.InputType); TypeSymbol? narrowedType = this.VisitType(node.NarrowedType); - return node.Update(declaredType, node.IsExplicitNotNullTest, inputType, narrowedType); + return node.Update(declaredType, node.IsExplicitNotNullTest, node.IsUnionMatching, inputType, narrowedType); } public override BoundNode? VisitBinaryPattern(BoundBinaryPattern node) { @@ -12461,14 +12629,14 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor BoundPattern negated = (BoundPattern)this.Visit(node.Negated); TypeSymbol? inputType = this.VisitType(node.InputType); TypeSymbol? narrowedType = this.VisitType(node.NarrowedType); - return node.Update(negated, inputType, narrowedType); + return node.Update(negated, node.IsUnionMatching, inputType, narrowedType); } public override BoundNode? VisitRelationalPattern(BoundRelationalPattern node) { BoundExpression value = (BoundExpression)this.Visit(node.Value); TypeSymbol? inputType = this.VisitType(node.InputType); TypeSymbol? narrowedType = this.VisitType(node.NarrowedType); - return node.Update(node.Relation, value, node.ConstantValue, inputType, narrowedType); + return node.Update(node.Relation, value, node.ConstantValue, node.IsUnionMatching, inputType, narrowedType); } public override BoundNode? VisitDiscardExpression(BoundDiscardExpression node) { @@ -12521,6 +12689,12 @@ internal abstract partial class BoundTreeRewriter : BoundTreeVisitor TypeSymbol? type = this.VisitType(node.Type); return node.Update(expression, node.NullableAnnotation, type); } + public override BoundNode? VisitValueForNullableAnalysis(BoundValueForNullableAnalysis node) + { + BoundExpression? originalExpression = (BoundExpression?)this.Visit(node.OriginalExpression); + TypeSymbol? type = this.VisitType(node.Type); + return node.Update(originalExpression, type); + } public override BoundNode? VisitWithExpression(BoundWithExpression node) { MethodSymbol? cloneMethod = this.VisitMethodSymbol(node.CloneMethod); @@ -13949,7 +14123,7 @@ public NullabilityRewriter(ImmutableDictionary deconstruction = this.VisitList(node.Deconstruction); ImmutableArray properties = this.VisitList(node.Properties); BoundExpression? variableAccess = (BoundExpression?)this.Visit(node.VariableAccess); - return node.Update(declaredType, deconstructMethod, deconstruction, properties, node.IsExplicitNotNullTest, variable, variableAccess, inputType, narrowedType); + return node.Update(declaredType, deconstructMethod, deconstruction, properties, node.IsExplicitNotNullTest, variable, variableAccess, node.IsUnionMatching, inputType, narrowedType); } public override BoundNode? VisitListPattern(BoundListPattern node) @@ -15077,7 +15262,7 @@ public NullabilityRewriter(ImmutableDictionary subpatterns = this.VisitList(node.Subpatterns); - return node.Update(getLengthMethod, getItemMethod, subpatterns, inputType, narrowedType); + return node.Update(getLengthMethod, getItemMethod, subpatterns, node.IsUnionMatching, inputType, narrowedType); } public override BoundNode? VisitPositionalSubpattern(BoundPositionalSubpattern node) @@ -15120,7 +15305,7 @@ public NullabilityRewriter(ImmutableDictionary new TreeDumperNode("decisionDag", null, new TreeDumperNode[] { new TreeDumperNode("rootNode", null, new TreeDumperNode[] { Visit(node.RootNode, null) }), + new TreeDumperNode("suitableForLowering", node.SuitableForLowering, null), new TreeDumperNode("hasErrors", node.HasErrors, null) } ); @@ -16569,6 +16773,12 @@ private BoundTreeDumperNodeProducer() new TreeDumperNode("hasErrors", node.HasErrors, null) } ); + public override TreeDumperNode VisitDagPassThroughEvaluation(BoundDagPassThroughEvaluation node, object? arg) => new TreeDumperNode("dagPassThroughEvaluation", null, new TreeDumperNode[] + { + new TreeDumperNode("input", null, new TreeDumperNode[] { Visit(node.Input, null) }), + new TreeDumperNode("hasErrors", node.HasErrors, null) + } + ); public override TreeDumperNode VisitSwitchSection(BoundSwitchSection node, object? arg) => new TreeDumperNode("switchSection", null, new TreeDumperNode[] { new TreeDumperNode("locals", node.Locals, null), @@ -16581,6 +16791,7 @@ private BoundTreeDumperNodeProducer() { new TreeDumperNode("label", node.Label, null), new TreeDumperNode("pattern", null, new TreeDumperNode[] { Visit(node.Pattern, null) }), + new TreeDumperNode("hasUnionMatching", node.HasUnionMatching, null), new TreeDumperNode("whenClause", null, new TreeDumperNode[] { Visit(node.WhenClause, null) }), new TreeDumperNode("hasErrors", node.HasErrors, null) } @@ -17226,6 +17437,7 @@ private BoundTreeDumperNodeProducer() { new TreeDumperNode("expression", null, new TreeDumperNode[] { Visit(node.Expression, null) }), new TreeDumperNode("pattern", null, new TreeDumperNode[] { Visit(node.Pattern, null) }), + new TreeDumperNode("hasUnionMatching", node.HasUnionMatching, null), new TreeDumperNode("isNegated", node.IsNegated, null), new TreeDumperNode("reachabilityDecisionDag", null, new TreeDumperNode[] { Visit(node.ReachabilityDecisionDag, null) }), new TreeDumperNode("whenTrueLabel", node.WhenTrueLabel, null), @@ -17239,6 +17451,18 @@ private BoundTreeDumperNodeProducer() { new TreeDumperNode("value", null, new TreeDumperNode[] { Visit(node.Value, null) }), new TreeDumperNode("constantValue", node.ConstantValue, null), + new TreeDumperNode("isUnionMatching", node.IsUnionMatching, null), + new TreeDumperNode("inputType", node.InputType, null), + new TreeDumperNode("narrowedType", node.NarrowedType, null), + new TreeDumperNode("hasErrors", node.HasErrors, null) + } + ); + public override TreeDumperNode VisitPatternWithUnionMatching(BoundPatternWithUnionMatching node, object? arg) => new TreeDumperNode("patternWithUnionMatching", null, new TreeDumperNode[] + { + new TreeDumperNode("unionMatchingInputType", node.UnionMatchingInputType, null), + new TreeDumperNode("leftOfPendingConjunction", null, new TreeDumperNode[] { Visit(node.LeftOfPendingConjunction, null) }), + new TreeDumperNode("valueProperty", null, new TreeDumperNode[] { Visit(node.ValueProperty, null) }), + new TreeDumperNode("valuePattern", null, new TreeDumperNode[] { Visit(node.ValuePattern, null) }), new TreeDumperNode("inputType", node.InputType, null), new TreeDumperNode("narrowedType", node.NarrowedType, null), new TreeDumperNode("hasErrors", node.HasErrors, null) @@ -17257,6 +17481,7 @@ private BoundTreeDumperNodeProducer() new TreeDumperNode("isVar", node.IsVar, null), new TreeDumperNode("variable", node.Variable, null), new TreeDumperNode("variableAccess", null, new TreeDumperNode[] { Visit(node.VariableAccess, null) }), + new TreeDumperNode("isUnionMatching", node.IsUnionMatching, null), new TreeDumperNode("inputType", node.InputType, null), new TreeDumperNode("narrowedType", node.NarrowedType, null), new TreeDumperNode("hasErrors", node.HasErrors, null) @@ -17271,6 +17496,7 @@ private BoundTreeDumperNodeProducer() new TreeDumperNode("isExplicitNotNullTest", node.IsExplicitNotNullTest, null), new TreeDumperNode("variable", node.Variable, null), new TreeDumperNode("variableAccess", null, new TreeDumperNode[] { Visit(node.VariableAccess, null) }), + new TreeDumperNode("isUnionMatching", node.IsUnionMatching, null), new TreeDumperNode("inputType", node.InputType, null), new TreeDumperNode("narrowedType", node.NarrowedType, null), new TreeDumperNode("hasErrors", node.HasErrors, null) @@ -17286,6 +17512,7 @@ private BoundTreeDumperNodeProducer() new TreeDumperNode("argumentPlaceholder", null, new TreeDumperNode[] { Visit(node.ArgumentPlaceholder, null) }), new TreeDumperNode("variable", node.Variable, null), new TreeDumperNode("variableAccess", null, new TreeDumperNode[] { Visit(node.VariableAccess, null) }), + new TreeDumperNode("isUnionMatching", node.IsUnionMatching, null), new TreeDumperNode("inputType", node.InputType, null), new TreeDumperNode("narrowedType", node.NarrowedType, null), new TreeDumperNode("hasErrors", node.HasErrors, null) @@ -17307,6 +17534,7 @@ private BoundTreeDumperNodeProducer() new TreeDumperNode("getLengthMethod", node.GetLengthMethod, null), new TreeDumperNode("getItemMethod", node.GetItemMethod, null), new TreeDumperNode("subpatterns", null, from x in node.Subpatterns select Visit(x, null)), + new TreeDumperNode("isUnionMatching", node.IsUnionMatching, null), new TreeDumperNode("inputType", node.InputType, null), new TreeDumperNode("narrowedType", node.NarrowedType, null), new TreeDumperNode("hasErrors", node.HasErrors, null) @@ -17339,6 +17567,7 @@ private BoundTreeDumperNodeProducer() { new TreeDumperNode("declaredType", null, new TreeDumperNode[] { Visit(node.DeclaredType, null) }), new TreeDumperNode("isExplicitNotNullTest", node.IsExplicitNotNullTest, null), + new TreeDumperNode("isUnionMatching", node.IsUnionMatching, null), new TreeDumperNode("inputType", node.InputType, null), new TreeDumperNode("narrowedType", node.NarrowedType, null), new TreeDumperNode("hasErrors", node.HasErrors, null) @@ -17357,6 +17586,7 @@ private BoundTreeDumperNodeProducer() public override TreeDumperNode VisitNegatedPattern(BoundNegatedPattern node, object? arg) => new TreeDumperNode("negatedPattern", null, new TreeDumperNode[] { new TreeDumperNode("negated", null, new TreeDumperNode[] { Visit(node.Negated, null) }), + new TreeDumperNode("isUnionMatching", node.IsUnionMatching, null), new TreeDumperNode("inputType", node.InputType, null), new TreeDumperNode("narrowedType", node.NarrowedType, null), new TreeDumperNode("hasErrors", node.HasErrors, null) @@ -17367,6 +17597,7 @@ private BoundTreeDumperNodeProducer() new TreeDumperNode("relation", node.Relation, null), new TreeDumperNode("value", null, new TreeDumperNode[] { Visit(node.Value, null) }), new TreeDumperNode("constantValue", node.ConstantValue, null), + new TreeDumperNode("isUnionMatching", node.IsUnionMatching, null), new TreeDumperNode("inputType", node.InputType, null), new TreeDumperNode("narrowedType", node.NarrowedType, null), new TreeDumperNode("hasErrors", node.HasErrors, null) @@ -17441,6 +17672,14 @@ private BoundTreeDumperNodeProducer() new TreeDumperNode("hasErrors", node.HasErrors, null) } ); + public override TreeDumperNode VisitValueForNullableAnalysis(BoundValueForNullableAnalysis node, object? arg) => new TreeDumperNode("valueForNullableAnalysis", null, new TreeDumperNode[] + { + new TreeDumperNode("originalExpression", null, new TreeDumperNode[] { Visit(node.OriginalExpression, null) }), + new TreeDumperNode("type", node.Type, null), + new TreeDumperNode("isSuppressed", node.IsSuppressed, null), + new TreeDumperNode("hasErrors", node.HasErrors, null) + } + ); public override TreeDumperNode VisitWithExpression(BoundWithExpression node, object? arg) => new TreeDumperNode("withExpression", null, new TreeDumperNode[] { new TreeDumperNode("receiver", null, new TreeDumperNode[] { Visit(node.Receiver, null) }), @@ -17491,7 +17730,10 @@ internal static PipelinePhase DoesNotSurvive(BoundKind kind) BoundKind.UnconvertedInterpolatedString => PipelinePhase.InitialBinding, BoundKind.InterpolatedStringHandlerPlaceholder => PipelinePhase.LocalRewriting, BoundKind.InterpolatedStringArgumentPlaceholder => PipelinePhase.LocalRewriting, + BoundKind.PatternWithUnionMatching => PipelinePhase.InitialBinding, BoundKind.DeconstructionVariablePendingInference => PipelinePhase.LocalRewriting, + BoundKind.ExpressionWithNullability => PipelinePhase.InitialBinding, + BoundKind.ValueForNullableAnalysis => PipelinePhase.InitialBinding, _ => PipelinePhase.Emit }; } diff --git a/src/Compilers/CSharp/Portable/Generated/CSharp.Generated.g4 b/src/Compilers/CSharp/Portable/Generated/CSharp.Generated.g4 index ef35f91bc48f..d6ff2982be77 100644 --- a/src/Compilers/CSharp/Portable/Generated/CSharp.Generated.g4 +++ b/src/Compilers/CSharp/Portable/Generated/CSharp.Generated.g4 @@ -347,7 +347,7 @@ record_declaration ; struct_declaration - : attribute_list* modifier* 'struct' identifier_token type_parameter_list? parameter_list? base_list? type_parameter_constraint_clause* '{'? member_declaration* '}'? ';'? + : attribute_list* modifier* ('struct' | 'union') identifier_token type_parameter_list? parameter_list? base_list? type_parameter_constraint_clause* '{'? member_declaration* '}'? ';'? ; delegate_declaration diff --git a/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Internal.Generated.cs b/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Internal.Generated.cs index 93bbb9d1eb3f..04507a068953 100644 --- a/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Internal.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Internal.Generated.cs @@ -16614,7 +16614,7 @@ internal TypeDeclarationSyntax(SyntaxKind kind) { } - /// Gets the type keyword token ("class", "struct", "interface", "record", "extension"). + /// Gets the type keyword token ("class", "struct", "interface", "record", "extension", "union"). public abstract SyntaxToken Keyword { get; } public abstract TypeParameterListSyntax? TypeParameterList { get; } @@ -17085,7 +17085,7 @@ internal StructDeclarationSyntax(SyntaxKind kind, GreenNode? attributeLists, Gre public override CoreSyntax.SyntaxList AttributeLists => new CoreSyntax.SyntaxList(this.attributeLists); public override CoreSyntax.SyntaxList Modifiers => new CoreSyntax.SyntaxList(this.modifiers); - /// Gets the struct keyword token. + /// Gets the struct or union keyword token. public override SyntaxToken Keyword => this.keyword; public override SyntaxToken Identifier => this.identifier; public override TypeParameterListSyntax? TypeParameterList => this.typeParameterList; @@ -17124,7 +17124,7 @@ public StructDeclarationSyntax Update(CoreSyntax.SyntaxList { if (attributeLists != this.AttributeLists || modifiers != this.Modifiers || keyword != this.Keyword || identifier != this.Identifier || typeParameterList != this.TypeParameterList || parameterList != this.ParameterList || baseList != this.BaseList || constraintClauses != this.ConstraintClauses || openBraceToken != this.OpenBraceToken || members != this.Members || closeBraceToken != this.CloseBraceToken || semicolonToken != this.SemicolonToken) { - var newNode = SyntaxFactory.StructDeclaration(attributeLists, modifiers, keyword, identifier, typeParameterList, parameterList, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); + var newNode = SyntaxFactory.StructDeclaration(this.Kind, attributeLists, modifiers, keyword, identifier, typeParameterList, parameterList, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); var diags = GetDiagnostics(); if (diags?.Length > 0) newNode = newNode.WithDiagnosticsGreen(diags); @@ -31796,11 +31796,22 @@ public ClassDeclarationSyntax ClassDeclaration(CoreSyntax.SyntaxList attributeLists, CoreSyntax.SyntaxList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, CoreSyntax.SyntaxList constraintClauses, SyntaxToken? openBraceToken, CoreSyntax.SyntaxList members, SyntaxToken? closeBraceToken, SyntaxToken? semicolonToken) + public StructDeclarationSyntax StructDeclaration(SyntaxKind kind, CoreSyntax.SyntaxList attributeLists, CoreSyntax.SyntaxList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, CoreSyntax.SyntaxList constraintClauses, SyntaxToken? openBraceToken, CoreSyntax.SyntaxList members, SyntaxToken? closeBraceToken, SyntaxToken? semicolonToken) { + switch (kind) + { + case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: break; + default: throw new ArgumentException(nameof(kind)); + } #if DEBUG if (keyword == null) throw new ArgumentNullException(nameof(keyword)); - if (keyword.Kind != SyntaxKind.StructKeyword) throw new ArgumentException(nameof(keyword)); + switch (keyword.Kind) + { + case SyntaxKind.StructKeyword: + case SyntaxKind.UnionKeyword: break; + default: throw new ArgumentException(nameof(keyword)); + } if (identifier == null) throw new ArgumentNullException(nameof(identifier)); if (identifier.Kind != SyntaxKind.IdentifierToken) throw new ArgumentException(nameof(identifier)); if (openBraceToken != null) @@ -31832,7 +31843,7 @@ public StructDeclarationSyntax StructDeclaration(CoreSyntax.SyntaxList attributeLists, CoreSyntax.SyntaxList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, CoreSyntax.SyntaxList constraintClauses, SyntaxToken? openBraceToken, CoreSyntax.SyntaxList members, SyntaxToken? closeBraceToken, SyntaxToken? semicolonToken) @@ -37184,11 +37195,22 @@ public static ClassDeclarationSyntax ClassDeclaration(CoreSyntax.SyntaxList attributeLists, CoreSyntax.SyntaxList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, CoreSyntax.SyntaxList constraintClauses, SyntaxToken? openBraceToken, CoreSyntax.SyntaxList members, SyntaxToken? closeBraceToken, SyntaxToken? semicolonToken) + public static StructDeclarationSyntax StructDeclaration(SyntaxKind kind, CoreSyntax.SyntaxList attributeLists, CoreSyntax.SyntaxList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, CoreSyntax.SyntaxList constraintClauses, SyntaxToken? openBraceToken, CoreSyntax.SyntaxList members, SyntaxToken? closeBraceToken, SyntaxToken? semicolonToken) { + switch (kind) + { + case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: break; + default: throw new ArgumentException(nameof(kind)); + } #if DEBUG if (keyword == null) throw new ArgumentNullException(nameof(keyword)); - if (keyword.Kind != SyntaxKind.StructKeyword) throw new ArgumentException(nameof(keyword)); + switch (keyword.Kind) + { + case SyntaxKind.StructKeyword: + case SyntaxKind.UnionKeyword: break; + default: throw new ArgumentException(nameof(keyword)); + } if (identifier == null) throw new ArgumentNullException(nameof(identifier)); if (identifier.Kind != SyntaxKind.IdentifierToken) throw new ArgumentException(nameof(identifier)); if (openBraceToken != null) @@ -37220,7 +37242,7 @@ public static StructDeclarationSyntax StructDeclaration(CoreSyntax.SyntaxList attributeLists, CoreSyntax.SyntaxList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, CoreSyntax.SyntaxList constraintClauses, SyntaxToken? openBraceToken, CoreSyntax.SyntaxList members, SyntaxToken? closeBraceToken, SyntaxToken? semicolonToken) diff --git a/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Main.Generated.cs b/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Main.Generated.cs index e8febb0d116f..fd2ccdeda321 100644 --- a/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Main.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Main.Generated.cs @@ -229,6 +229,7 @@ public partial class CSharpSyntaxVisitor public virtual TResult? VisitSpreadElement(SpreadElementSyntax node) => this.DefaultVisit(node); /// Called when the visitor visits a WithElementSyntax node. + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] public virtual TResult? VisitWithElement(WithElementSyntax node) => this.DefaultVisit(node); /// Called when the visitor visits a QueryExpressionSyntax node. @@ -976,6 +977,7 @@ public partial class CSharpSyntaxVisitor public virtual void VisitSpreadElement(SpreadElementSyntax node) => this.DefaultVisit(node); /// Called when the visitor visits a WithElementSyntax node. + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] public virtual void VisitWithElement(WithElementSyntax node) => this.DefaultVisit(node); /// Called when the visitor visits a QueryExpressionSyntax node. @@ -1722,6 +1724,7 @@ public partial class CSharpSyntaxRewriter : CSharpSyntaxVisitor public override SyntaxNode? VisitSpreadElement(SpreadElementSyntax node) => node.Update(VisitToken(node.OperatorToken), (ExpressionSyntax?)Visit(node.Expression) ?? throw new ArgumentNullException("expression")); + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] public override SyntaxNode? VisitWithElement(WithElementSyntax node) => node.Update(VisitToken(node.WithKeyword), (ArgumentListSyntax?)Visit(node.ArgumentList) ?? throw new ArgumentNullException("argumentList")); @@ -3446,6 +3449,7 @@ public static SpreadElementSyntax SpreadElement(ExpressionSyntax expression) => SyntaxFactory.SpreadElement(SyntaxFactory.Token(SyntaxKind.DotDotToken), expression); /// Creates a new WithElementSyntax instance. + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] public static WithElementSyntax WithElement(SyntaxToken withKeyword, ArgumentListSyntax argumentList) { if (withKeyword.Kind() != SyntaxKind.WithKeyword) throw new ArgumentException(nameof(withKeyword)); @@ -3454,6 +3458,7 @@ public static WithElementSyntax WithElement(SyntaxToken withKeyword, ArgumentLis } /// Creates a new WithElementSyntax instance. + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] public static WithElementSyntax WithElement(ArgumentListSyntax? argumentList = default) => SyntaxFactory.WithElement(SyntaxFactory.Token(SyntaxKind.WithKeyword), argumentList ?? SyntaxFactory.ArgumentList()); @@ -5013,9 +5018,21 @@ public static ClassDeclarationSyntax ClassDeclaration(SyntaxListCreates a new StructDeclarationSyntax instance. - public static StructDeclarationSyntax StructDeclaration(SyntaxList attributeLists, SyntaxTokenList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, SyntaxList constraintClauses, SyntaxToken openBraceToken, SyntaxList members, SyntaxToken closeBraceToken, SyntaxToken semicolonToken) + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82567")] + public static StructDeclarationSyntax StructDeclaration(SyntaxKind kind, SyntaxList attributeLists, SyntaxTokenList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, SyntaxList constraintClauses, SyntaxToken openBraceToken, SyntaxList members, SyntaxToken closeBraceToken, SyntaxToken semicolonToken) { - if (keyword.Kind() != SyntaxKind.StructKeyword) throw new ArgumentException(nameof(keyword)); + switch (kind) + { + case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: break; + default: throw new ArgumentException(nameof(kind)); + } + switch (keyword.Kind()) + { + case SyntaxKind.StructKeyword: + case SyntaxKind.UnionKeyword: break; + default: throw new ArgumentException(nameof(keyword)); + } if (identifier.Kind() != SyntaxKind.IdentifierToken) throw new ArgumentException(nameof(identifier)); switch (openBraceToken.Kind()) { @@ -5035,9 +5052,17 @@ public static StructDeclarationSyntax StructDeclaration(SyntaxList(), modifiers.Node.ToGreenList(), (Syntax.InternalSyntax.SyntaxToken)keyword.Node!, (Syntax.InternalSyntax.SyntaxToken)identifier.Node!, typeParameterList == null ? null : (Syntax.InternalSyntax.TypeParameterListSyntax)typeParameterList.Green, parameterList == null ? null : (Syntax.InternalSyntax.ParameterListSyntax)parameterList.Green, baseList == null ? null : (Syntax.InternalSyntax.BaseListSyntax)baseList.Green, constraintClauses.Node.ToGreenList(), (Syntax.InternalSyntax.SyntaxToken?)openBraceToken.Node, members.Node.ToGreenList(), (Syntax.InternalSyntax.SyntaxToken?)closeBraceToken.Node, (Syntax.InternalSyntax.SyntaxToken?)semicolonToken.Node).CreateRed(); + return (StructDeclarationSyntax)Syntax.InternalSyntax.SyntaxFactory.StructDeclaration(kind, attributeLists.Node.ToGreenList(), modifiers.Node.ToGreenList(), (Syntax.InternalSyntax.SyntaxToken)keyword.Node!, (Syntax.InternalSyntax.SyntaxToken)identifier.Node!, typeParameterList == null ? null : (Syntax.InternalSyntax.TypeParameterListSyntax)typeParameterList.Green, parameterList == null ? null : (Syntax.InternalSyntax.ParameterListSyntax)parameterList.Green, baseList == null ? null : (Syntax.InternalSyntax.BaseListSyntax)baseList.Green, constraintClauses.Node.ToGreenList(), (Syntax.InternalSyntax.SyntaxToken?)openBraceToken.Node, members.Node.ToGreenList(), (Syntax.InternalSyntax.SyntaxToken?)closeBraceToken.Node, (Syntax.InternalSyntax.SyntaxToken?)semicolonToken.Node).CreateRed(); } + private static SyntaxKind GetStructDeclarationKeywordKind(SyntaxKind kind) + => kind switch + { + SyntaxKind.StructDeclaration => SyntaxKind.StructKeyword, + SyntaxKind.UnionDeclaration => SyntaxKind.UnionKeyword, + _ => throw new ArgumentOutOfRangeException(), + }; + /// Creates a new InterfaceDeclarationSyntax instance. public static InterfaceDeclarationSyntax InterfaceDeclaration(SyntaxList attributeLists, SyntaxTokenList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, SyntaxList constraintClauses, SyntaxToken openBraceToken, SyntaxList members, SyntaxToken closeBraceToken, SyntaxToken semicolonToken) { diff --git a/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Syntax.Generated.cs b/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Syntax.Generated.cs index 15426d48b5bd..5b059a12f773 100644 --- a/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Syntax.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/CSharpSyntaxGenerator/CSharpSyntaxGenerator.SourceGenerator/Syntax.xml.Syntax.Generated.cs @@ -4279,6 +4279,7 @@ public SpreadElementSyntax Update(SyntaxToken operatorToken, ExpressionSyntax ex /// /// /// +[Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] public sealed partial class WithElementSyntax : CollectionElementSyntax { private ArgumentListSyntax? argumentList; @@ -10314,7 +10315,7 @@ internal TypeDeclarationSyntax(InternalSyntax.CSharpSyntaxNode green, SyntaxNode { } - /// Gets the type keyword token ("class", "struct", "interface", "record", "extension"). + /// Gets the type keyword token ("class", "struct", "interface", "record", "extension", "union"). public abstract SyntaxToken Keyword { get; } public TypeDeclarationSyntax WithKeyword(SyntaxToken keyword) => WithKeywordCore(keyword); internal abstract TypeDeclarationSyntax WithKeywordCore(SyntaxToken keyword); @@ -10529,6 +10530,7 @@ public ClassDeclarationSyntax Update(SyntaxList attributeLi /// This node is associated with the following syntax kinds: /// /// +/// /// /// public sealed partial class StructDeclarationSyntax : TypeDeclarationSyntax @@ -10556,7 +10558,7 @@ public override SyntaxTokenList Modifiers } } - /// Gets the struct keyword token. + /// Gets the struct or union keyword token. public override SyntaxToken Keyword => new SyntaxToken(this, ((InternalSyntax.StructDeclarationSyntax)this.Green).keyword, GetChildPosition(2), GetChildIndex(2)); public override SyntaxToken Identifier => new SyntaxToken(this, ((InternalSyntax.StructDeclarationSyntax)this.Green).identifier, GetChildPosition(3), GetChildIndex(3)); @@ -10629,7 +10631,7 @@ public StructDeclarationSyntax Update(SyntaxList attributeL { if (attributeLists != this.AttributeLists || modifiers != this.Modifiers || keyword != this.Keyword || identifier != this.Identifier || typeParameterList != this.TypeParameterList || parameterList != this.ParameterList || baseList != this.BaseList || constraintClauses != this.ConstraintClauses || openBraceToken != this.OpenBraceToken || members != this.Members || closeBraceToken != this.CloseBraceToken || semicolonToken != this.SemicolonToken) { - var newNode = SyntaxFactory.StructDeclaration(attributeLists, modifiers, keyword, identifier, typeParameterList, parameterList, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); + var newNode = SyntaxFactory.StructDeclaration(this.Kind(), attributeLists, modifiers, keyword, identifier, typeParameterList, parameterList, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); var annotations = GetAnnotations(); return annotations?.Length > 0 ? newNode.WithAnnotations(annotations) : newNode; } diff --git a/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs b/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs index f990e748549a..c0a24a1e7285 100644 --- a/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs +++ b/src/Compilers/CSharp/Portable/Generated/ErrorFacts.Generated.cs @@ -346,6 +346,8 @@ public static bool IsWarning(ErrorCode code) case ErrorCode.WRN_UnscopedRefAttributeOldRules: case ErrorCode.WRN_InterceptsLocationAttributeUnsupportedSignature: case ErrorCode.WRN_RedundantPattern: + case ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules: + case ErrorCode.WRN_UnsafeMeaningless: return true; default: return false; diff --git a/src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/RuntimeAsyncRewriter.cs b/src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/RuntimeAsyncRewriter.cs index 394ff90149de..cf1b8eadedcc 100644 --- a/src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/RuntimeAsyncRewriter.cs +++ b/src/Compilers/CSharp/Portable/Lowering/AsyncRewriter/RuntimeAsyncRewriter.cs @@ -193,7 +193,8 @@ private BoundExpression RewriteCustomAwaiterAwait(BoundAwaitExpression node) _placeholderMap.Remove(awaitableInfo.RuntimeAsyncAwaitCallPlaceholder); // if (!_tmp.IsCompleted) awaitCall - var ifNotCompleted = _factory.If(_factory.Not(isCompletedCall), _factory.ExpressionStatement(awaitCall)); + var ifNotCompleted = _factory.HiddenSequencePoint( + _factory.If(_factory.Not(isCompletedCall), _factory.ExpressionStatement(awaitCall))); // _tmp.GetResult() var getResultMethod = awaitableInfo.GetResult; diff --git a/src/Compilers/CSharp/Portable/Lowering/ClosureConversion/ExpressionLambdaRewriter.cs b/src/Compilers/CSharp/Portable/Lowering/ClosureConversion/ExpressionLambdaRewriter.cs index e650763905b0..d93bbe5b0175 100644 --- a/src/Compilers/CSharp/Portable/Lowering/ClosureConversion/ExpressionLambdaRewriter.cs +++ b/src/Compilers/CSharp/Portable/Lowering/ClosureConversion/ExpressionLambdaRewriter.cs @@ -700,6 +700,8 @@ private BoundExpression VisitConversion(BoundConversion node) var mg = (BoundMethodGroup)node.Operand; return DelegateCreation(mg.ReceiverOpt, node.SymbolOpt, node.Type, !node.SymbolOpt.RequiresInstanceReceiver && !node.IsExtensionMethod); } + case ConversionKind.Union: + throw ExceptionUtilities.UnexpectedValue(node.ConversionKind); case ConversionKind.ExplicitUserDefined: case ConversionKind.ImplicitUserDefined: case ConversionKind.IntPtr: diff --git a/src/Compilers/CSharp/Portable/Lowering/ClosureConversion/SynthesizedClosureMethod.cs b/src/Compilers/CSharp/Portable/Lowering/ClosureConversion/SynthesizedClosureMethod.cs index 5ce6f0704245..060135445b2d 100644 --- a/src/Compilers/CSharp/Portable/Lowering/ClosureConversion/SynthesizedClosureMethod.cs +++ b/src/Compilers/CSharp/Portable/Lowering/ClosureConversion/SynthesizedClosureMethod.cs @@ -172,6 +172,11 @@ private static DeclarationModifiers MakeDeclarationModifiers(ClosureKind closure mods |= DeclarationModifiers.Extern; } + if (originalMethod is LocalFunctionOrSourceMemberMethodSymbol { IsUnsafe: true }) + { + mods |= DeclarationModifiers.Unsafe; + } + return mods; } @@ -217,7 +222,7 @@ protected override ImmutableArray ExtraSynthesizedRefParameters internal int ExtraSynthesizedParameterCount => this._structEnvironments.IsDefault ? 0 : this._structEnvironments.Length; internal override bool InheritsBaseMethodAttributes => true; - internal override bool GenerateDebugInfo => !this.IsAsync; + internal override bool GenerateDebugInfo => !this.IsAsync || DeclaringCompilation.IsRuntimeAsyncEnabledIn(this); internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree localTree) { diff --git a/src/Compilers/CSharp/Portable/Lowering/DiagnosticsPass_ExpressionTrees.cs b/src/Compilers/CSharp/Portable/Lowering/DiagnosticsPass_ExpressionTrees.cs index ffa6f296fbec..f9b093a392cc 100644 --- a/src/Compilers/CSharp/Portable/Lowering/DiagnosticsPass_ExpressionTrees.cs +++ b/src/Compilers/CSharp/Portable/Lowering/DiagnosticsPass_ExpressionTrees.cs @@ -276,6 +276,7 @@ public override BoundNode VisitEventAccess(BoundEventAccess node) { bool hasBaseReceiver = node.ReceiverOpt != null && node.ReceiverOpt.Kind == BoundKind.BaseReference; Binder.ReportDiagnosticsIfObsolete(_diagnostics, node.EventSymbol.AssociatedField, node.Syntax, hasBaseReceiver, _containingSymbol, _containingSymbol.ContainingType, BinderFlags.None); + Binder.AssertNotUnsafeMemberAccess(node.EventSymbol.AssociatedField); } CheckReceiverIfField(node.ReceiverOpt); return base.VisitEventAccess(node); @@ -290,6 +291,7 @@ public override BoundNode VisitEventAssignmentOperator(BoundEventAssignmentOpera bool hasBaseReceiver = node.ReceiverOpt != null && node.ReceiverOpt.Kind == BoundKind.BaseReference; Binder.ReportDiagnosticsIfObsolete(_diagnostics, node.Event, ((AssignmentExpressionSyntax)node.Syntax).Left, hasBaseReceiver, _containingSymbol, _containingSymbol.ContainingType, BinderFlags.None); + // Unsafe member access is checked on the accessor only to avoid duplicate diagnostics. CheckReceiverIfField(node.ReceiverOpt); return base.VisitEventAssignmentOperator(node); } @@ -893,6 +895,13 @@ public override BoundNode VisitConversion(BoundConversion node) } break; + case ConversionKind.Union: + if (_inExpressionLambda) + { + Error(ErrorCode.ERR_ExpressionTreeContainsUnionConversion, node); + } + break; + default: if (_inExpressionLambda && node.Conversion.Method is MethodSymbol method && (method.IsAbstract || method.IsVirtual) && method.IsStatic) diff --git a/src/Compilers/CSharp/Portable/Lowering/Instrumentation/LocalStateTracingInstrumenter.cs b/src/Compilers/CSharp/Portable/Lowering/Instrumentation/LocalStateTracingInstrumenter.cs index 8fd5993664c2..442bc1a844d3 100644 --- a/src/Compilers/CSharp/Portable/Lowering/Instrumentation/LocalStateTracingInstrumenter.cs +++ b/src/Compilers/CSharp/Portable/Lowering/Instrumentation/LocalStateTracingInstrumenter.cs @@ -298,7 +298,9 @@ public override void InstrumentBlock(BoundBlock original, LocalRewriter rewriter Debug.Assert(_factory.TopLevelMethod is not null); Debug.Assert(_factory.CurrentFunction is not null); - var isStateMachine = _factory.CurrentFunction.IsAsync || _factory.CurrentFunction.IsIterator; + var currentFunction = _factory.CurrentFunction; + var isStateMachine = (currentFunction.IsAsync && !_factory.Compilation.IsRuntimeAsyncEnabledIn(currentFunction)) + || currentFunction.IsIterator; var prologueBuilder = ArrayBuilder.GetInstance(_factory.CurrentFunction.ParameterCount); diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.DecisionDagRewriter.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.DecisionDagRewriter.cs index 40362f6dce8e..0e26e09a9e3a 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.DecisionDagRewriter.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.DecisionDagRewriter.cs @@ -551,7 +551,7 @@ private ValueDispatchNode GatherValueDispatchNodes( HashSet loweredNodes, BoundDagTemp input) { - IValueSetFactory fac = ValueSetFactory.ForInput(input); + IConstantValueSetFactory fac = ValueSetFactory.ForInput(input); return GatherValueDispatchNodes(node, loweredNodes, input, fac); } @@ -559,7 +559,7 @@ private ValueDispatchNode GatherValueDispatchNodes( BoundDecisionDagNode node, HashSet loweredNodes, BoundDagTemp input, - IValueSetFactory fac) + IConstantValueSetFactory fac) { if (loweredNodes.Contains(node)) { @@ -618,7 +618,7 @@ private ValueDispatchNode PushEqualityTestsIntoTree( SyntaxNode syntax, ValueDispatchNode otherwise, ImmutableArray<(ConstantValue value, LabelSymbol label)> cases, - IValueSetFactory fac) + IConstantValueSetFactory fac) { if (cases.IsEmpty) return otherwise; @@ -703,7 +703,7 @@ private void LowerRelationalDispatchNode(ValueDispatchNode.RelationalDispatch re /// private sealed class CasesComparer : IComparer<(ConstantValue value, LabelSymbol label)> { - private readonly IValueSetFactory _fac; + private readonly IConstantValueSetFactory _fac; public CasesComparer(TypeSymbol type) { _fac = ValueSetFactory.ForType(type); diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.PatternLocalRewriter.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.PatternLocalRewriter.cs index 68e372f41e36..1feb72a75060 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.PatternLocalRewriter.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter.PatternLocalRewriter.cs @@ -140,7 +140,7 @@ protected BoundExpression LowerEvaluation(BoundDagEvaluation evaluation) case BoundDagFieldEvaluation f: { FieldSymbol field = f.Field; - var outputTemp = new BoundDagTemp(f.Syntax, field.Type, f); + var outputTemp = f.MakeResultTemp(); BoundExpression output = _tempAllocator.GetTemp(outputTemp); BoundExpression access = _localRewriter.MakeFieldAccess(f.Syntax, input, field, null, LookupResultKind.Viable, field.Type); access.WasCompilerGenerated = true; @@ -150,7 +150,7 @@ protected BoundExpression LowerEvaluation(BoundDagEvaluation evaluation) case BoundDagPropertyEvaluation p: { PropertySymbol property = p.Property; - var outputTemp = new BoundDagTemp(p.Syntax, property.Type, p); + var outputTemp = p.MakeResultTemp(); BoundExpression output = _tempAllocator.GetTemp(outputTemp); // Tracked by https://github.com/dotnet/roslyn/issues/78827 : MQ, Consider preserving the BoundConversion from initial binding instead of using markAsChecked here input = _localRewriter.ConvertReceiverForExtensionMemberIfNeeded(property, input, markAsChecked: true); @@ -169,33 +169,44 @@ void addArg(RefKind refKind, BoundExpression expression) argBuilder.Add(expression); } - Debug.Assert(method.Name == WellKnownMemberNames.DeconstructMethodName); - int extensionExtra; + Debug.Assert(method.Name is WellKnownMemberNames.DeconstructMethodName or WellKnownMemberNames.TryGetValueMethodName); if (method.IsStatic) { Debug.Assert(method.IsExtensionMethod); receiver = _factory.Type(method.ContainingType); // Tracked by https://github.com/dotnet/roslyn/issues/78827 : MQ, Consider preserving the BoundConversion from initial binding instead of using markAsChecked here addArg(method.ParameterRefKinds[0], _localRewriter.ConvertReceiverForExtensionIfNeeded(input, markAsChecked: true, method.Parameters[0])); - extensionExtra = 1; } else { receiver = input; - extensionExtra = 0; } - for (int i = extensionExtra; i < method.ParameterCount; i++) + ArrayBuilder outParamTemps = d.MakeOutParameterTemps(); + foreach (var outputTemp in outParamTemps) { - ParameterSymbol parameter = method.Parameters[i]; - Debug.Assert(parameter.RefKind == RefKind.Out); - var outputTemp = new BoundDagTemp(d.Syntax, parameter.Type, d, i - extensionExtra); addArg(RefKind.Out, _tempAllocator.GetTemp(outputTemp)); } + outParamTemps.Free(); + + BoundExpression returnValue = null; + if (!method.ReturnsVoid) + { + Debug.Assert(method.Name is WellKnownMemberNames.TryGetValueMethodName); + Debug.Assert(method.ReturnType.SpecialType == SpecialType.System_Boolean); + returnValue = _tempAllocator.GetTemp(d.MakeReturnValueTemp()); + } // Tracked by https://github.com/dotnet/roslyn/issues/78827 : MQ, Consider preserving the BoundConversion from initial binding instead of using markAsChecked here receiver = _localRewriter.ConvertReceiverForExtensionMemberIfNeeded(method, receiver, markAsChecked: true); - return _factory.Call(receiver, method, refKindBuilder.ToImmutableAndFree(), argBuilder.ToImmutableAndFree()); + BoundExpression result = _factory.Call(receiver, method, refKindBuilder.ToImmutableAndFree(), argBuilder.ToImmutableAndFree()); + + if (returnValue is not null) + { + result = _factory.AssignmentExpression(returnValue, result); + } + + return result; } case BoundDagTypeEvaluation t: @@ -211,12 +222,13 @@ void addArg(RefKind refKind, BoundExpression expression) } TypeSymbol type = t.Type; - var outputTemp = new BoundDagTemp(t.Syntax, type, t); + var outputTemp = t.MakeResultTemp(); BoundExpression output = _tempAllocator.GetTemp(outputTemp); CompoundUseSiteInfo useSiteInfo = _localRewriter.GetNewCompoundUseSiteInfo(); Conversion conversion = _factory.Compilation.Conversions.ClassifyBuiltInConversion(inputType, output.Type, isChecked: false, ref useSiteInfo); Debug.Assert(!conversion.IsUserDefined); + Debug.Assert(!conversion.IsUnion); _localRewriter._diagnostics.Add(t.Syntax, useSiteInfo); BoundExpression evaluated; if (conversion.Exists) @@ -249,7 +261,7 @@ void addArg(RefKind refKind, BoundExpression expression) Debug.Assert(e.Property.GetMethod.ParameterCount == 1); Debug.Assert(e.Property.GetMethod.Parameters[0].Type.SpecialType == SpecialType.System_Int32); TypeSymbol type = e.Property.GetMethod.ReturnType; - var outputTemp = new BoundDagTemp(e.Syntax, type, e); + var outputTemp = e.MakeResultTemp(); BoundExpression output = _tempAllocator.GetTemp(outputTemp); return _factory.AssignmentExpression(output, _factory.Indexer(input, e.Property, _factory.Literal(e.Index))); } @@ -274,7 +286,7 @@ void addArg(RefKind refKind, BoundExpression expression) var access = (BoundExpression)_localRewriter.Visit(indexerAccess); - var outputTemp = new BoundDagTemp(e.Syntax, e.IndexerType, e); + var outputTemp = e.MakeResultTemp(); BoundExpression output = _tempAllocator.GetTemp(outputTemp); return _factory.AssignmentExpression(output, access); } @@ -300,12 +312,22 @@ void addArg(RefKind refKind, BoundExpression expression) var access = (BoundExpression)_localRewriter.Visit(indexerAccess); - var outputTemp = new BoundDagTemp(e.Syntax, e.SliceType, e); + var outputTemp = e.MakeResultTemp(); BoundExpression output = _tempAllocator.GetTemp(outputTemp); return _factory.AssignmentExpression(output, access); } - case BoundDagAssignmentEvaluation: + case BoundDagAssignmentEvaluation e: + { + Debug.Assert(!e.Target.Equals(e.Input)); + BoundExpression left = _tempAllocator.GetTemp(e.Target); + BoundExpression right = _tempAllocator.GetTemp(e.Input); + return _factory.AssignmentExpression(left, right); + } + case BoundDagPassThroughEvaluation: + { + return input; + } default: throw ExceptionUtilities.UnexpectedValue(evaluation); } @@ -481,10 +503,10 @@ protected bool TryLowerTypeTestAndCast( evaluation is BoundDagTypeEvaluation typeEvaluation1 && typeDecision.Type.IsReferenceType && typeEvaluation1.Type.Equals(typeDecision.Type, TypeCompareKind.AllIgnoreOptions) && - typeEvaluation1.Input == typeDecision.Input) + typeEvaluation1.Input.Equals(typeDecision.Input)) { BoundExpression input = _tempAllocator.GetTemp(test.Input); - BoundExpression output = _tempAllocator.GetTemp(new BoundDagTemp(evaluation.Syntax, typeEvaluation1.Type, evaluation)); + BoundExpression output = _tempAllocator.GetTemp(evaluation.MakeResultTemp()); Debug.Assert(output.Type is { }); sideEffect = _factory.AssignmentExpression(output, _factory.As(input, typeEvaluation1.Type)); testExpression = _factory.ObjectNotEqual(output, _factory.Null(output.Type)); @@ -500,7 +522,7 @@ evaluation is BoundDagTypeEvaluation typeEvaluation2 && { BoundExpression input = _tempAllocator.GetTemp(test.Input); var baseType = typeEvaluation2.Type; - BoundExpression output = _tempAllocator.GetTemp(new BoundDagTemp(evaluation.Syntax, baseType, evaluation)); + BoundExpression output = _tempAllocator.GetTemp(evaluation.MakeResultTemp()); sideEffect = _factory.AssignmentExpression(output, _factory.Convert(baseType, input, conv)); testExpression = _factory.ObjectNotEqual(output, _factory.Null(baseType)); _localRewriter._diagnostics.Add(test.Syntax, useSiteInfo); @@ -637,7 +659,7 @@ private BoundDecisionDag RewriteTupleInput( Debug.Assert(field != null); var expr = loweredInput.Arguments[i]; var fieldFetchEvaluation = new BoundDagFieldEvaluation(expr.Syntax, field, originalInput); - var temp = new BoundDagTemp(expr.Syntax, expr.Type, fieldFetchEvaluation); + var temp = fieldFetchEvaluation.MakeResultTemp(); storeToTemp(temp, expr); newArguments.Add(_tempAllocator.GetTemp(temp)); } diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs index ff7f6cd65bed..11175d27f0e8 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Call.cs @@ -614,6 +614,7 @@ private static bool IsSafeForReordering(BoundExpression expression, RefKind kind case ConversionKind.ExplicitUserDefined: case ConversionKind.ImplicitUserDefined: + case ConversionKind.Union: // https://github.com/dotnet/roslyn/issues/82636: Add coverage // expression trees rewrite this later. // it is a kind of user defined conversions on IntPtr and in some cases can fail case ConversionKind.IntPtr: diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Conversion.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Conversion.cs index 8ef3d126f948..a7faa56a70df 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Conversion.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Conversion.cs @@ -357,6 +357,12 @@ private BoundExpression MakeConversionNodeCore( @checked: @checked, rewrittenType: rewrittenType); + case ConversionKind.Union: + return RewriteUnionConversion( + syntax: syntax, + rewrittenOperand: rewrittenOperand, + conversion: conversion); + case ConversionKind.IntPtr: return RewriteIntPtrConversion(syntax, rewrittenOperand, conversion, @checked, explicitCastInCode, constantValueOpt, rewrittenType); @@ -945,6 +951,53 @@ internal BoundExpression MakeConversionNode( return userDefined; } + if (conversion.IsUnion) + { + UserDefinedConversionAnalysis analysis = conversion.BestUnionConversionAnalysis; + conversion.AssertUnderlyingConversionsCheckedRecursive(); + Debug.Assert(analysis.Kind == UserDefinedConversionAnalysisKind.ApplicableInNormalForm); + Debug.Assert(analysis.Operator is { MethodKind: MethodKind.Constructor, ParameterCount: 1 }); + Debug.Assert(TypeSymbol.Equals(analysis.FromType, analysis.Operator.GetParameterType(0), TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(TypeSymbol.Equals(rewrittenType.StrippedType(), analysis.Operator.ContainingType, TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(TypeSymbol.Equals(rewrittenType.StrippedType(), analysis.ToType, TypeCompareKind.AllIgnoreOptions)); + Debug.Assert(analysis.TargetConversion is { IsIdentity: true } or { IsNullable: true, IsImplicit: true }); + + if (!TypeSymbol.Equals(rewrittenOperand.Type, analysis.FromType, TypeCompareKind.AllIgnoreOptions)) + { + Debug.Assert(!analysis.SourceConversion.IsIdentity); + rewrittenOperand = MakeConversionNode( + rewrittenOperand.Syntax, + rewrittenOperand, + analysis.SourceConversion, + analysis.FromType, + @checked); + } + + Debug.Assert(TypeSymbol.Equals(rewrittenOperand.Type, analysis.Operator.GetParameterType(0), TypeCompareKind.AllIgnoreOptions)); + + rewrittenOperand = RewriteUnionConversion( + syntax, + rewrittenOperand, + conversion); + + Debug.Assert(TypeSymbol.Equals(rewrittenOperand.Type, analysis.ToType, TypeCompareKind.AllIgnoreOptions)); + + if (!TypeSymbol.Equals(rewrittenOperand.Type, rewrittenType, TypeCompareKind.AllIgnoreOptions)) + { + Debug.Assert(!analysis.TargetConversion.IsIdentity); + rewrittenOperand = MakeConversionNode( + rewrittenOperand.Syntax, + rewrittenOperand, + analysis.TargetConversion, + rewrittenType, + @checked); + } + + Debug.Assert(TypeSymbol.Equals(rewrittenOperand.Type, rewrittenType, TypeCompareKind.AllIgnoreOptions)); + + return rewrittenOperand; + } + return MakeConversionNode( oldNodeOpt: null, syntax: syntax, @@ -1495,6 +1548,32 @@ private BoundExpression RewriteLiftedUserDefinedConversion( type: rewrittenType); } + private BoundExpression RewriteUnionConversion( + SyntaxNode syntax, + BoundExpression rewrittenOperand, + Conversion conversion) + { + Debug.Assert(conversion.IsUnion); + Debug.Assert(conversion.Method is { MethodKind: MethodKind.Constructor, ParameterCount: 1 }); + Debug.Assert(rewrittenOperand.Type is { }); + Debug.Assert(!_inExpressionLambda); + Debug.Assert(conversion.Method.Parameters[0].Type.Equals(rewrittenOperand.Type, TypeCompareKind.AllIgnoreOptions)); + + var constructor = conversion.Method; + return new BoundObjectCreationExpression( + syntax, + constructor, + [rewrittenOperand], + argumentNamesOpt: default, + SyntheticBoundNodeFactory.ArgumentRefKindsFromParameterRefKinds(constructor, useStrictArgumentRefKinds: false), + expanded: false, + argsToParamsOpt: default, + defaultArguments: default, + constantValueOpt: null, + initializerExpressionOpt: null, + constructor.ContainingType); + } + private BoundExpression RewriteIntPtrConversion( SyntaxNode syntax, BoundExpression rewrittenOperand, @@ -1807,6 +1886,11 @@ private Conversion TryMakeConversion(SyntaxNode syntax, Conversion conversion, T return resultConversion; } } + case ConversionKind.Union: + { + // https://github.com/dotnet/roslyn/issues/82636: Confirm + throw ExceptionUtilities.UnexpectedValue(conversion.Kind); + } case ConversionKind.IntPtr: { Debug.Assert(!_compilation.Assembly.RuntimeSupportsNumericIntPtr); diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ForEachStatement.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ForEachStatement.cs index c574c9a9ca87..27c11633527c 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ForEachStatement.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ForEachStatement.cs @@ -293,13 +293,18 @@ private BoundStatement RewriteForEachEnumerator( private bool TryGetDisposeMethod(SyntaxNode forEachSyntax, ForEachEnumeratorInfo enumeratorInfo, out MethodSymbol disposeMethod) { - if (enumeratorInfo.IsAsync) + return TryGetDisposeMethod(_compilation, forEachSyntax, enumeratorInfo.IsAsync, _diagnostics, out disposeMethod); + } + + internal static bool TryGetDisposeMethod(CSharpCompilation compilation, SyntaxNode syntax, bool isAsync, BindingDiagnosticBag diagnostics, out MethodSymbol disposeMethod) + { + if (isAsync) { - disposeMethod = (MethodSymbol)Binder.GetWellKnownTypeMember(_compilation, WellKnownMember.System_IAsyncDisposable__DisposeAsync, _diagnostics, syntax: forEachSyntax); + disposeMethod = (MethodSymbol)Binder.GetWellKnownTypeMember(compilation, WellKnownMember.System_IAsyncDisposable__DisposeAsync, diagnostics, syntax: syntax); return (object)disposeMethod != null; } - return Binder.TryGetSpecialTypeMember(_compilation, SpecialMember.System_IDisposable__Dispose, forEachSyntax, _diagnostics, out disposeMethod); + return Binder.TryGetSpecialTypeMember(compilation, SpecialMember.System_IDisposable__Dispose, syntax, diagnostics, out disposeMethod); } /// @@ -351,6 +356,7 @@ private bool TryGetDisposeMethod(SyntaxNode forEachSyntax, ForEachEnumeratorInfo containingMember: _factory.CurrentFunction, containingType: _factory.CurrentType, location: enumeratorInfo.Location); + // Unsafe member access diagnostics reported during binding. if (implementsInterface || !(enumeratorInfo.PatternDisposeInfo is null)) { @@ -501,7 +507,9 @@ private BoundExpression ConvertReceiverForInvocation(CSharpSyntaxNode syntax, Bo Debug.Assert(receiver.Type is { }); if (!receiver.Type.IsReferenceType && method.ContainingType.IsInterface) { - Debug.Assert(receiverConversion.IsImplicit && !receiverConversion.IsUserDefined); + Debug.Assert(receiverConversion.IsImplicit); + Debug.Assert(!receiverConversion.IsUserDefined); + Debug.Assert(!receiverConversion.IsUnion); // NOTE: The spec says that disposing of a struct enumerator won't cause any // unnecessary boxing to occur. However, Dev10 extends this improvement to the diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_IndexerAccess.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_IndexerAccess.cs index e817e4723606..c9e1ea075b97 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_IndexerAccess.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_IndexerAccess.cs @@ -2,12 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Immutable; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.CodeGen; using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.PooledObjects; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp { @@ -287,47 +288,65 @@ private BoundExpression MakeIndexerAccess( PatternIndexOffsetLoweringStrategy startStrategy, endStrategy; RewriteRangeParts(node.Argument, out rangeExpr, out startMakeOffsetInput, out startStrategy, out endMakeOffsetInput, out endStrategy, out rewrittenRangeArg); - var localsBuilder = ArrayBuilder.GetInstance(); - var sideEffectsBuilder = ArrayBuilder.GetInstance(); - - BoundExpression startExpr; - BoundExpression rangeSizeExpr; - if (rangeExpr is not null) + // For open-ended ranges like `startExpr..`, use Slice(int) instead of Slice(int, int) when available + if (rangeExpr is not null && endMakeOffsetInput is null && + TryGetStartOnlyOverload(getItemOrSliceHelper, node.Syntax) is MethodSymbol startOnlyOverload) { - - startExpr = makePatternIndexOffsetExpression(startMakeOffsetInput, length, startStrategy); - BoundExpression endExpr = makePatternIndexOffsetExpression(endMakeOffsetInput, length, endStrategy); - rangeSizeExpr = MakeRangeSize(ref startExpr, endExpr, localsBuilder, sideEffectsBuilder); + BoundExpression startExpr = makePatternIndexOffsetExpression(startMakeOffsetInput, length, startStrategy); + if (isInt32ConstantZero(startExpr)) + { + // Start is 0, so the result is the full span. No need for Slice at all. + result = _factory.Call(null, createSpan, rewrittenReceiver, _factory.Literal(length), useStrictArgumentRefKinds: true); + } + else + { + var spanExpr = _factory.Call(null, createSpan, rewrittenReceiver, _factory.Literal(length), useStrictArgumentRefKinds: true); + result = _factory.Call(spanExpr, startOnlyOverload, startExpr); + } } else { - Debug.Assert(rewrittenRangeArg is not null); - DeconstructRange(rewrittenRangeArg, _factory.Literal(length), localsBuilder, sideEffectsBuilder, out startExpr, out rangeSizeExpr); - } + var localsBuilder = ArrayBuilder.GetInstance(); + var sideEffectsBuilder = ArrayBuilder.GetInstance(); - BoundExpression possiblyRefCapturedReceiver = rewrittenReceiver; + BoundExpression startExpr; + BoundExpression rangeSizeExpr; + if (rangeExpr is not null) + { + startExpr = makePatternIndexOffsetExpression(startMakeOffsetInput, length, startStrategy); + BoundExpression endExpr = makePatternIndexOffsetExpression(endMakeOffsetInput, length, endStrategy); + rangeSizeExpr = MakeRangeSize(ref startExpr, endExpr, localsBuilder, sideEffectsBuilder); + } + else + { + Debug.Assert(rewrittenRangeArg is not null); + DeconstructRange(rewrittenRangeArg, _factory.Literal(length), localsBuilder, sideEffectsBuilder, out startExpr, out rangeSizeExpr); + } - if (sideEffectsBuilder.Count != 0) - { - possiblyRefCapturedReceiver = _factory.StoreToTemp(possiblyRefCapturedReceiver, out var refCapture, createSpan.Parameters[0].RefKind == RefKind.In ? RefKindExtensions.StrictIn : RefKind.Ref); - localsBuilder.Insert(0, ((BoundLocal)possiblyRefCapturedReceiver).LocalSymbol); - sideEffectsBuilder.Insert(0, refCapture); - } + BoundExpression possiblyRefCapturedReceiver = rewrittenReceiver; - if (startExpr.ConstantValueOpt is { SpecialType: SpecialType.System_Int32, Int32Value: 0 } && - rangeSizeExpr.ConstantValueOpt is { SpecialType: SpecialType.System_Int32, Int32Value: >= 0 and int rangeSizeConst } && - rangeSizeConst <= length) - { - // No need to call Slice, we can create a Span of the right length from the start. - result = _factory.Call(null, createSpan, possiblyRefCapturedReceiver, rangeSizeExpr, useStrictArgumentRefKinds: true); - } - else - { - result = _factory.Call(_factory.Call(null, createSpan, possiblyRefCapturedReceiver, _factory.Literal(length), useStrictArgumentRefKinds: true), - getItemOrSliceHelper, startExpr, rangeSizeExpr); - } + if (sideEffectsBuilder.Count != 0) + { + possiblyRefCapturedReceiver = _factory.StoreToTemp(possiblyRefCapturedReceiver, out var refCapture, createSpan.Parameters[0].RefKind == RefKind.In ? RefKindExtensions.StrictIn : RefKind.Ref); + localsBuilder.Insert(0, ((BoundLocal)possiblyRefCapturedReceiver).LocalSymbol); + sideEffectsBuilder.Insert(0, refCapture); + } + + if (isInt32ConstantZero(startExpr) && + rangeSizeExpr.ConstantValueOpt is { SpecialType: SpecialType.System_Int32, Int32Value: >= 0 and int rangeSizeConst } && + rangeSizeConst <= length) + { + // No need to call Slice, we can create a Span of the right length from the start. + result = _factory.Call(null, createSpan, possiblyRefCapturedReceiver, rangeSizeExpr, useStrictArgumentRefKinds: true); + } + else + { + result = _factory.Call(_factory.Call(null, createSpan, possiblyRefCapturedReceiver, _factory.Literal(length), useStrictArgumentRefKinds: true), + getItemOrSliceHelper, startExpr, rangeSizeExpr); + } - result = _factory.Sequence(localsBuilder.ToImmutableAndFree(), sideEffectsBuilder.ToImmutableAndFree(), result); + result = _factory.Sequence(localsBuilder.ToImmutableAndFree(), sideEffectsBuilder.ToImmutableAndFree(), result); + } } } @@ -340,6 +359,11 @@ private BoundExpression MakeIndexerAccess( return result; + static bool isInt32ConstantZero(BoundExpression expr) + { + return expr.ConstantValueOpt is { SpecialType: SpecialType.System_Int32, Int32Value: 0 }; + } + BoundExpression makePatternIndexOffsetExpression(BoundExpression? makeOffsetInput, int length, PatternIndexOffsetLoweringStrategy strategy) { BoundExpression integerArgument; @@ -890,8 +914,35 @@ private BoundExpression VisitRangePatternIndexerAccess(BoundImplicitIndexerAcces BoundExpression startExpr; BoundExpression rangeSizeExpr; + var sliceCall = (BoundCall)node.IndexerOrSliceAccess; if (rangeExpr is not null) { + BoundExpression? lengthAccess = null; + Debug.Assert(startStrategy is not PatternIndexOffsetLoweringStrategy.Length); + Debug.Assert(endMakeOffsetInput is not null || endStrategy == PatternIndexOffsetLoweringStrategy.Length); + + // For open-ended ranges like `start..`, use Slice(int) or Substring(int) when available + if (!cacheAllArgumentsOnly && + endMakeOffsetInput is null && + TryGetStartOnlyOverload(sliceCall.Method, node.Syntax) is MethodSymbol startOnlyOverload) + { + if (startStrategy is PatternIndexOffsetLoweringStrategy.SubtractFromLength or PatternIndexOffsetLoweringStrategy.UseGetOffsetAPI) + { + if (startStrategy == PatternIndexOffsetLoweringStrategy.SubtractFromLength) + { + Debug.Assert(startMakeOffsetInput is not null); + storeExpressionIfNotConstant(ref startMakeOffsetInput, localsBuilder, sideEffectsBuilder); + } + + lengthAccess = VisitExpression(node.LengthOrCountAccess); + } + + startExpr = MakePatternIndexOffsetExpression(startMakeOffsetInput, lengthAccess, startStrategy); + + RemovePlaceholderReplacement(node.ReceiverPlaceholder); + return F.Call(receiver, startOnlyOverload, startExpr); + } + // If we know that the input is a range expression, we can // optimize by pulling it apart inline, so // @@ -961,27 +1012,15 @@ private BoundExpression VisitRangePatternIndexerAccess(BoundImplicitIndexerAcces if ((rewriteFlags & captureStartOffset) != 0) { Debug.Assert(startMakeOffsetInput is not null); - if (startMakeOffsetInput.ConstantValueOpt is null) - { - startMakeOffsetInput = F.StoreToTemp(startMakeOffsetInput, out BoundAssignmentOperator inputStore); - localsBuilder.Add(((BoundLocal)startMakeOffsetInput).LocalSymbol); - sideEffectsBuilder.Add(inputStore); - } + storeExpressionIfNotConstant(ref startMakeOffsetInput, localsBuilder, sideEffectsBuilder); } if ((rewriteFlags & captureEndOffset) != 0) { Debug.Assert(endMakeOffsetInput is not null); - if (endMakeOffsetInput.ConstantValueOpt is null) - { - endMakeOffsetInput = F.StoreToTemp(endMakeOffsetInput, out BoundAssignmentOperator inputStore); - localsBuilder.Add(((BoundLocal)endMakeOffsetInput).LocalSymbol); - sideEffectsBuilder.Add(inputStore); - } + storeExpressionIfNotConstant(ref endMakeOffsetInput, localsBuilder, sideEffectsBuilder); } - BoundExpression? lengthAccess = null; - if ((rewriteFlags & useLength) != 0) { lengthAccess = VisitExpression(node.LengthOrCountAccess); @@ -1010,7 +1049,7 @@ private BoundExpression VisitRangePatternIndexerAccess(BoundImplicitIndexerAcces var rangeSizeLocal = F.StoreToTemp(rangeSizeExpr, out var rangeSizeStore); localsBuilder.Add(rangeSizeLocal.LocalSymbol); sideEffectsBuilder.Add(rangeSizeStore); - rangeSizeExpr = startLocal; + rangeSizeExpr = rangeSizeLocal; } } else @@ -1023,7 +1062,6 @@ private BoundExpression VisitRangePatternIndexerAccess(BoundImplicitIndexerAcces AddPlaceholderReplacement(node.ArgumentPlaceholders[0], startExpr); AddPlaceholderReplacement(node.ArgumentPlaceholders[1], rangeSizeExpr); - var sliceCall = (BoundCall)node.IndexerOrSliceAccess; var rewrittenIndexerAccess = VisitExpression(sliceCall); RemovePlaceholderReplacement(node.ArgumentPlaceholders[0]); @@ -1031,6 +1069,80 @@ private BoundExpression VisitRangePatternIndexerAccess(BoundImplicitIndexerAcces RemovePlaceholderReplacement(node.ReceiverPlaceholder); return rewrittenIndexerAccess; + + void storeExpressionIfNotConstant([DisallowNull] ref BoundExpression? expression, ArrayBuilder localsBuilder, ArrayBuilder sideEffectsBuilder) + { + if (expression.ConstantValueOpt is null) + { + expression = this._factory.StoreToTemp(expression, out BoundAssignmentOperator store); + localsBuilder.Add(((BoundLocal)expression).LocalSymbol); + sideEffectsBuilder.Add(store); + } + } + } + + /// + /// For known types (string, Span, ReadOnlySpan, Memory, ReadOnlyMemory), + /// tries to find a one-argument overload (e.g., Slice(int) or Substring(int)) + /// corresponding to the given two-argument method (e.g., Slice(int, int) or Substring(int, int)). + /// + private MethodSymbol? TryGetStartOnlyOverload(MethodSymbol method, SyntaxNode syntax) + { + // 1. string.Substring(int, int) → string.Substring(int) + MethodSymbol? startLengthOverload; + MethodSymbol? startOverload; + + if (method is { Name: nameof(string.Substring), ContainingType.SpecialType: SpecialType.System_String } + && TryGetSpecialTypeMethod(syntax, SpecialMember.System_String__SubstringIntInt, out startLengthOverload, isOptional: true) + && ReferenceEquals(method, startLengthOverload) + && TryGetSpecialTypeMethod(syntax, SpecialMember.System_String__SubstringInt, out startOverload, isOptional: true)) + { + Debug.Assert(startLengthOverload.Name == startOverload.Name + && startLengthOverload.ReturnType.Equals(startOverload.ReturnType, TypeCompareKind.ConsiderEverything)); + + return startOverload; + } + + // 2. various Slice(int, int) → Slice(int) cases + if (method is not { Name: WellKnownMemberNames.SliceMethodName, OriginalDefinition: var originalDefinition, ContainingType: NamedTypeSymbol containingType }) + { + return null; + } + + startOverload = containingType.Name switch + { + nameof(Span<>) => tryGetWellKnownSliceStartOverload(WellKnownMember.System_Span_T__Slice_Int_Int, WellKnownMember.System_Span_T__Slice_Int, originalDefinition, syntax), + nameof(ReadOnlySpan<>) => tryGetWellKnownSliceStartOverload(WellKnownMember.System_ReadOnlySpan_T__Slice_Int_Int, WellKnownMember.System_ReadOnlySpan_T__Slice_Int, originalDefinition, syntax), + nameof(Memory<>) => tryGetWellKnownSliceStartOverload(WellKnownMember.System_Memory_T__Slice_Int_Int, WellKnownMember.System_Memory_T__Slice_Int, originalDefinition, syntax), + nameof(ReadOnlyMemory<>) => tryGetWellKnownSliceStartOverload(WellKnownMember.System_ReadOnlyMemory_T__Slice_Int_Int, WellKnownMember.System_ReadOnlyMemory_T__Slice_Int, originalDefinition, syntax), + _ => null, + }; + + if (startOverload is null) + { + return null; + } + + return startOverload.AsMember(containingType); + + MethodSymbol? tryGetWellKnownSliceStartOverload(WellKnownMember startLengthMember, WellKnownMember startMember, MethodSymbol methodDefinition, SyntaxNode syntax) + { + Debug.Assert(methodDefinition.IsDefinition); + MethodSymbol? startLengthOverload; + MethodSymbol? startOverload; + + if (!this.TryGetWellKnownTypeMember(syntax, startLengthMember, out startLengthOverload, isOptional: true) + || !ReferenceEquals(methodDefinition, startLengthOverload) + || !this.TryGetWellKnownTypeMember(syntax, startMember, out startOverload, isOptional: true)) + { + return null; + } + + Debug.Assert(startLengthOverload.Name == startOverload.Name + && startLengthOverload.ReturnType.Equals(startOverload.ReturnType, TypeCompareKind.ConsiderEverything)); + + return startOverload; + } } private BoundExpression MakeRangeSize(ref BoundExpression startExpr, BoundExpression endExpr, ArrayBuilder localsBuilder, ArrayBuilder sideEffectsBuilder) diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ReturnStatement.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ReturnStatement.cs index 5b45b90a7b7a..b2fd906bb312 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ReturnStatement.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_ReturnStatement.cs @@ -20,13 +20,16 @@ public override BoundNode VisitReturnStatement(BoundReturnStatement node) // We do this to ensure that expression lambdas and expression-bodied // properties have sequence points. // We also add sequence points for the implicit "return" statement at the end of the method body - // (added by FlowAnalysisPass.AppendImplicitReturn). Implicitly added return for async method - // does not need sequence points added here since it would be done later (presumably during Async rewrite). + // (added by FlowAnalysisPass.AppendImplicitReturn). Implicitly added return for async method + // does not need sequence points added here since it would be done later (presumably during Async rewrite), + // except in runtime async where the method body is emitted directly. + var currentFunction = _factory.CurrentFunction; + var isRuntimeAsync = currentFunction is not null && _compilation.IsRuntimeAsyncEnabledIn(currentFunction); if (this.Instrument && (!node.WasCompilerGenerated || (node.ExpressionOpt != null ? IsLambdaOrExpressionBodiedMember : - (node.Syntax.Kind() == SyntaxKind.Block && _factory.CurrentFunction?.IsAsync == false)))) + (node.Syntax.Kind() == SyntaxKind.Block && (currentFunction?.IsAsync == false || isRuntimeAsync))))) { rewritten = Instrumenter.InstrumentReturnStatement(node, rewritten); } diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_TupleBinaryOperator.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_TupleBinaryOperator.cs index d776681eb89c..ca90605104b6 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_TupleBinaryOperator.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_TupleBinaryOperator.cs @@ -196,7 +196,7 @@ private BoundExpression DeferSideEffectingArgumentToTempForTupleEquality( case BoundConversion { Conversion: { Kind: var conversionKind } conversion } when conversionMustBePerformedOnOriginalExpression(conversionKind): // Some conversions cannot be performed on a copy of the argument and must be done early. return EvaluateSideEffectingArgumentToTemp(expr, effects, temps); - case BoundConversion { Conversion: { IsUserDefined: true } } conv when conv.ExplicitCastInCode || enclosingConversionWasExplicit: + case BoundConversion { Conversion: { IsUserDefined: true } or { IsUnion: true } } conv when conv.ExplicitCastInCode || enclosingConversionWasExplicit: // https://github.com/dotnet/roslyn/issues/82636: Add coverage // A user-defined conversion triggered by a cast must be performed early. return EvaluateSideEffectingArgumentToTemp(expr, effects, temps); case BoundConversion conv: @@ -406,7 +406,7 @@ BoundExpression makeNullableHasValue(BoundExpression expr) case BoundConversion { Conversion: { IsIdentity: true }, Operand: var o }: return makeNullableHasValue(o); case BoundConversion { Conversion: { IsNullable: true, UnderlyingConversions: var underlying } conversion, Operand: var o } - when expr.Type.IsNullableType() && o.Type is { } && o.Type.IsNullableType() && !underlying[0].IsUserDefined: + when expr.Type.IsNullableType() && o.Type is { } && o.Type.IsNullableType() && !underlying[0].IsUserDefined: // https://github.com/dotnet/roslyn/issues/82636: Confirm union conversions don't need special handling here // Note that a user-defined conversion from K to Nullable which may translate // a non-null K to a null value gives rise to a lifted conversion from Nullable to Nullable with the same property. // We therefore do not attempt to optimize nullable conversions with an underlying user-defined conversion. diff --git a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Yield.cs b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Yield.cs index 6b0b84b17e0d..0e155b983a02 100644 --- a/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Yield.cs +++ b/src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_Yield.cs @@ -15,10 +15,14 @@ public override BoundNode VisitYieldBreakStatement(BoundYieldBreakStatement node var result = (BoundStatement)base.VisitYieldBreakStatement(node)!; // We also add sequence points for the implicit "yield break" statement at the end of the method body - // (added by FlowAnalysisPass.AppendImplicitReturn). Implicitly added "yield break" for async method - // does not need sequence points added here since it would be done later (presumably during Async rewrite). + // (added by FlowAnalysisPass.AppendImplicitReturn). Implicitly added "yield break" for async method + // does not need sequence points added here since it would be done later (presumably during Async rewrite), + // except in runtime async where the method body is emitted directly. + // This will need additional testing when async iterators are emitted with runtime async. https://github.com/dotnet/roslyn/issues/75960 + var currentFunction = _factory.CurrentFunction; + var isRuntimeAsync = currentFunction is not null && _compilation.IsRuntimeAsyncEnabledIn(currentFunction); if (this.Instrument && - (!node.WasCompilerGenerated || (node.Syntax.Kind() == SyntaxKind.Block && _factory.CurrentFunction?.IsAsync == false))) + (!node.WasCompilerGenerated || (node.Syntax.Kind() == SyntaxKind.Block && (currentFunction?.IsAsync == false || isRuntimeAsync)))) { result = Instrumenter.InstrumentYieldBreakStatement(node, result); } diff --git a/src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/StateMachineTypeSymbol.cs b/src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/StateMachineTypeSymbol.cs index 3756651f2dad..0af8e557cec6 100644 --- a/src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/StateMachineTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/StateMachineTypeSymbol.cs @@ -87,5 +87,7 @@ public sealed override ImmutableArray GetAttributes() internal override bool HasCodeAnalysisEmbeddedAttribute => false; internal override bool HasCompilerLoweringPreserveAttribute => false; + + internal override bool IsUnionTypeCore => false; } } diff --git a/src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/SynthesizedStateMachineProperty.cs b/src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/SynthesizedStateMachineProperty.cs index 8d02f6293939..ce83ab93573f 100644 --- a/src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/SynthesizedStateMachineProperty.cs +++ b/src/Compilers/CSharp/Portable/Lowering/StateMachineRewriter/SynthesizedStateMachineProperty.cs @@ -155,6 +155,8 @@ public override bool IsExtern internal sealed override bool HasUnscopedRefAttribute => false; + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal override ObsoleteAttributeData ObsoleteAttributeData { get { return null; } diff --git a/src/Compilers/CSharp/Portable/Lowering/SynthesizedMethodBaseSymbol.cs b/src/Compilers/CSharp/Portable/Lowering/SynthesizedMethodBaseSymbol.cs index c51726fa3823..2a5fab97aee2 100644 --- a/src/Compilers/CSharp/Portable/Lowering/SynthesizedMethodBaseSymbol.cs +++ b/src/Compilers/CSharp/Portable/Lowering/SynthesizedMethodBaseSymbol.cs @@ -213,5 +213,10 @@ public sealed override bool IsImplicitlyDeclared { get { return true; } } + + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => + InheritsBaseMethodAttributes + ? BaseMethod.RuntimeAsyncMethodGenerationAttributeSetting + : ThreeState.Unknown; } } diff --git a/src/Compilers/CSharp/Portable/Lowering/SyntheticBoundNodeFactory.cs b/src/Compilers/CSharp/Portable/Lowering/SyntheticBoundNodeFactory.cs index 899210d5ecd0..918ddfc4a777 100644 --- a/src/Compilers/CSharp/Portable/Lowering/SyntheticBoundNodeFactory.cs +++ b/src/Compilers/CSharp/Portable/Lowering/SyntheticBoundNodeFactory.cs @@ -1495,7 +1495,7 @@ private MethodSymbol GetFieldFromHandleMethod(NamedTypeSymbol fieldContainer) /// /// It is intentional that there is no 'Convert' helper that calls this method automatically - /// and then calls . + /// and then calls . /// For the benefit of clarity and readability, consumer is expected to assert at the use-site /// what specific conversions are expected as the result of classification. /// @@ -1512,7 +1512,7 @@ public Conversion ClassifyEmitConversion(BoundExpression arg, TypeSymbol destina /// /// Note, this API is expected to be called only for that is natively supported by Emit layer. /// - public BoundExpression Convert(TypeSymbol type, BoundExpression arg, Conversion conversion, bool isChecked = false) + public BoundExpression Convert(TypeSymbol type, BoundExpression arg, Conversion conversion, bool isChecked = false, bool explicitCastInCode = true) { CodeGen.CodeGenerator.AssertIsEmitConversionKind(conversion.Kind); @@ -1527,7 +1527,7 @@ public BoundExpression Convert(TypeSymbol type, BoundExpression arg, Conversion } Debug.Assert(arg.Type is { }); - return new BoundConversion(Syntax, arg, conversion, @checked: isChecked, explicitCastInCode: true, conversionGroupOpt: null, InConversionGroupFlags.Unspecified, null, type) { WasCompilerGenerated = true }; + return new BoundConversion(Syntax, arg, conversion, @checked: isChecked, explicitCastInCode: explicitCastInCode, conversionGroupOpt: null, InConversionGroupFlags.Unspecified, null, type) { WasCompilerGenerated = true }; } public BoundExpression ArrayOrEmpty(TypeSymbol elementType, BoundExpression[] elements) diff --git a/src/Compilers/CSharp/Portable/Operations/CSharpOperationFactory.cs b/src/Compilers/CSharp/Portable/Operations/CSharpOperationFactory.cs index b4eaa80ef177..a8049c2cde61 100644 --- a/src/Compilers/CSharp/Portable/Operations/CSharpOperationFactory.cs +++ b/src/Compilers/CSharp/Portable/Operations/CSharpOperationFactory.cs @@ -1030,9 +1030,34 @@ private ILocalFunctionOperation CreateBoundLocalFunctionStatementOperation(Bound private IOperation CreateBoundConversionOperation(BoundConversion boundConversion, bool forceOperandImplicitLiteral = false) { Debug.Assert(!forceOperandImplicitLiteral || boundConversion.Operand is BoundLiteral); - bool isImplicit = boundConversion.WasCompilerGenerated || !boundConversion.ExplicitCastInCode || forceOperandImplicitLiteral; BoundExpression boundOperand = boundConversion.Operand; + bool isImplicit; + + if (boundConversion.WasCompilerGenerated || forceOperandImplicitLiteral) + { + isImplicit = true; + } + else if (boundConversion.ConversionGroupOpt?.Conversion.IsUnion == true && + (boundConversion.InConversionGroupFlags & InConversionGroupFlags.UnionSourceConversion) == 0) + { + boundConversion.TryGetUnionConversionParts(out BoundConversion? sourceConversion, out BoundConversion? unionConversion, out _); + + if (unionConversion is not null) + { + isImplicit = !unionConversion.ExplicitCastInCode || boundConversion.Syntax == (sourceConversion ?? unionConversion).Operand.Syntax; + } + else + { + ExceptionUtilities.UnexpectedValue(boundConversion); // Unexpected tree shape + isImplicit = !boundConversion.ExplicitCastInCode; + } + } + else + { + isImplicit = !boundConversion.ExplicitCastInCode; + } + if (boundConversion.ConversionKind == ConversionKind.InterpolatedStringHandler) { Debug.Assert(!forceOperandImplicitLiteral); @@ -1101,7 +1126,7 @@ private IOperation CreateBoundConversionOperation(BoundConversion boundConversio Debug.Assert(!forceOperandImplicitLiteral); return Create(boundOperand); } - else + else if (!boundOperand.WasCompilerGenerated) { // Make this conversion implicit isImplicit = true; diff --git a/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs b/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs index c1a20f78319f..4cd18c65ae42 100644 --- a/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs +++ b/src/Compilers/CSharp/Portable/Parser/LanguageParser.cs @@ -798,6 +798,7 @@ MemberDeclarationSyntax adjustStateAndReportStatementOutOfOrder(ref NamespacePar case SyntaxKind.FileScopedNamespaceDeclaration: case SyntaxKind.EnumDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: case SyntaxKind.ClassDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.DelegateDeclaration: @@ -1420,9 +1421,9 @@ nextToken.Kind is SyntaxKind.EnumKeyword or SyntaxKind.DelegateKeyword || // this is a partial ref struct declaration { var next = PeekToken(1); - if (isStructOrRecordKeyword(next) || + if (isStructOrRecordOrUnionKeyword(next) || (next.ContextualKind == SyntaxKind.PartialKeyword && - isStructOrRecordKeyword(PeekToken(2)))) + isStructOrRecordOrUnionKeyword(PeekToken(2)))) { modTok = this.EatToken(); } @@ -1491,20 +1492,32 @@ bool parseAsModifier(MessageID requiredFeature, [NotNullWhen(true)] out SyntaxTo return true; } - bool isStructOrRecordKeyword(SyntaxToken token) + bool isStructOrRecordOrUnionKeyword(SyntaxToken token) { if (token.Kind == SyntaxKind.StructKeyword) { return true; } - if (token.ContextualKind == SyntaxKind.RecordKeyword) + switch (token.ContextualKind) { - // This is an unusual use of LangVersion. Normally we only produce errors when the langversion - // does not support a feature, but in this case we are effectively making a language breaking - // change to consider "record" a type declaration in all ambiguous cases. To avoid breaking - // older code that is not using C# 9 we conditionally parse based on langversion - return IsFeatureEnabled(MessageID.IDS_FeatureRecords); + case SyntaxKind.RecordKeyword: + { + // This is an unusual use of LangVersion. Normally we only produce errors when the langversion + // does not support a feature, but in this case we are effectively making a language breaking + // change to consider "record" a type declaration in all ambiguous cases. To avoid breaking + // older code that is not using C# 9 we conditionally parse based on langversion + return IsFeatureEnabled(MessageID.IDS_FeatureRecords); + } + + case SyntaxKind.UnionKeyword: + { + // This is an unusual use of LangVersion. Normally we only produce errors when the langversion + // does not support a feature, but in this case we are effectively making a language breaking + // change to consider "union" a type declaration in all ambiguous cases. To avoid breaking + // older code that is not using C# 15 we conditionally parse based on langversion + return IsFeatureEnabled(MessageID.IDS_FeatureUnions); + } } return false; @@ -1641,13 +1654,25 @@ private bool IsPartialType() return true; } - if (nextToken.ContextualKind == SyntaxKind.RecordKeyword) + switch (nextToken.ContextualKind) { - // This is an unusual use of LangVersion. Normally we only produce errors when the langversion - // does not support a feature, but in this case we are effectively making a language breaking - // change to consider "record" a type declaration in all ambiguous cases. To avoid breaking - // older code that is not using C# 9 we conditionally parse based on langversion - return IsFeatureEnabled(MessageID.IDS_FeatureRecords); + case SyntaxKind.RecordKeyword: + { + // This is an unusual use of LangVersion. Normally we only produce errors when the langversion + // does not support a feature, but in this case we are effectively making a language breaking + // change to consider "record" a type declaration in all ambiguous cases. To avoid breaking + // older code that is not using C# 9 we conditionally parse based on langversion + return IsFeatureEnabled(MessageID.IDS_FeatureRecords); + } + + case SyntaxKind.UnionKeyword: + { + // This is an unusual use of LangVersion. Normally we only produce errors when the langversion + // does not support a feature, but in this case we are effectively making a language breaking + // change to consider "union" a type declaration in all ambiguous cases. To avoid breaking + // older code that is not using C# 15 we conditionally parse based on langversion + return IsFeatureEnabled(MessageID.IDS_FeatureUnions); + } } return false; @@ -1729,7 +1754,7 @@ private MemberDeclarationSyntax ParseTypeDeclaration(SyntaxList attributes, SyntaxListBuilder modifiers) { Debug.Assert(this.CurrentToken.Kind is SyntaxKind.ClassKeyword or SyntaxKind.StructKeyword or SyntaxKind.InterfaceKeyword || - this.CurrentToken.ContextualKind is SyntaxKind.RecordKeyword or SyntaxKind.ExtensionKeyword); + this.CurrentToken.ContextualKind is SyntaxKind.RecordKeyword or SyntaxKind.ExtensionKeyword or SyntaxKind.UnionKeyword); // "top-level" expressions and statements should never occur inside an asynchronous context Debug.Assert(!IsInAsync); @@ -1751,6 +1776,7 @@ private TypeDeclarationSyntax ParseMainTypeDeclaration(SyntaxList); if (this.CurrentToken.ContextualKind == SyntaxKind.WhereKeyword) @@ -3833,7 +3875,7 @@ private ConversionOperatorDeclarationSyntax TryParseConversionOperatorDeclaratio type = ParseIdentifierName(); } - var paramList = this.ParseParenthesizedParameterList(forExtension: false); + var paramList = this.ParseParenthesizedParameterList(forExtensionOrUnion: false); this.ParseBlockAndExpressionBodiesWithSemicolon(out var blockBody, out var expressionBody, out var semicolon); @@ -4059,7 +4101,7 @@ private MemberDeclarationSyntax ParseOperatorDeclaration( } var opKind = opToken.Kind; - var paramList = this.ParseParenthesizedParameterList(forExtension: false); + var paramList = this.ParseParenthesizedParameterList(forExtensionOrUnion: false); switch (paramList.Parameters.Count) { @@ -4709,14 +4751,14 @@ private static SyntaxKind GetAccessorKind(SyntaxToken accessorName) }; } - internal ParameterListSyntax ParseParenthesizedParameterList(bool forExtension) + internal ParameterListSyntax ParseParenthesizedParameterList(bool forExtensionOrUnion) { - if (this.IsIncrementalAndFactoryContextMatches && CanReuseParameterList(this.CurrentNode as CSharp.Syntax.ParameterListSyntax, allowOptionalIdentifier: forExtension)) + if (this.IsIncrementalAndFactoryContextMatches && CanReuseParameterList(this.CurrentNode as CSharp.Syntax.ParameterListSyntax, identifierIsOptional: forExtensionOrUnion)) { return (ParameterListSyntax)this.EatNode(); } - var parameters = this.ParseParameterList(out var open, out var close, SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken, forExtension); + var parameters = this.ParseParameterList(out var open, out var close, SyntaxKind.OpenParenToken, SyntaxKind.CloseParenToken, forExtensionOrUnion); return _syntaxFactory.ParameterList(open, parameters, close); } @@ -4727,11 +4769,11 @@ internal BracketedParameterListSyntax ParseBracketedParameterList() return (BracketedParameterListSyntax)this.EatNode(); } - var parameters = this.ParseParameterList(out var open, out var close, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken, forExtension: false); + var parameters = this.ParseParameterList(out var open, out var close, SyntaxKind.OpenBracketToken, SyntaxKind.CloseBracketToken, forExtensionOrUnion: false); return _syntaxFactory.BracketedParameterList(open, parameters, close); } - private static bool CanReuseParameterList(Syntax.ParameterListSyntax list, bool allowOptionalIdentifier) + private static bool CanReuseParameterList(Syntax.ParameterListSyntax list, bool identifierIsOptional) { if (list == null) { @@ -4750,7 +4792,7 @@ private static bool CanReuseParameterList(Syntax.ParameterListSyntax list, bool foreach (var parameter in list.Parameters) { - if (!CanReuseParameter(parameter, allowOptionalIdentifier)) + if (!CanReuseParameter(parameter, identifierIsOptional)) { return false; } @@ -4778,7 +4820,7 @@ private static bool CanReuseBracketedParameterList(Syntax.BracketedParameterList foreach (var parameter in list.Parameters) { - if (!CanReuseParameter(parameter, allowOptionalIdentifier: false)) + if (!CanReuseParameter(parameter, identifierIsOptional: false)) { return false; } @@ -4792,16 +4834,16 @@ private SeparatedSyntaxList ParseParameterList( out SyntaxToken close, SyntaxKind openKind, SyntaxKind closeKind, - bool forExtension) + bool forExtensionOrUnion) { open = this.EatToken(openKind); var saveTerm = _termState; _termState |= TerminatorState.IsEndOfParameterList; - Func parseElement = forExtension - ? static @this => @this.ParseParameter(allowOptionalIdentifier: true) - : static @this => @this.ParseParameter(allowOptionalIdentifier: false); + Func parseElement = forExtensionOrUnion + ? static @this => @this.ParseParameter(identifierIsOptional: true) + : static @this => @this.ParseParameter(identifierIsOptional: false); var parameters = ParseCommaSeparatedSyntaxList( ref open, @@ -4810,7 +4852,7 @@ private SeparatedSyntaxList ParseParameterList( parseElement, skipBadParameterListTokens, allowTrailingSeparator: false, - requireOneElement: forExtension, // For extension declarations, we require at least one receiver parameter + requireOneElement: forExtensionOrUnion, // For extension/union declarations, we require at least one parameter allowSemicolonAsSeparator: false); _termState = saveTerm; @@ -4853,7 +4895,7 @@ private bool IsPossibleParameter() } } - private static bool CanReuseParameter(CSharp.Syntax.ParameterSyntax parameter, bool allowOptionalIdentifier) + private static bool CanReuseParameter(CSharp.Syntax.ParameterSyntax parameter, bool identifierIsOptional) { if (parameter == null) { @@ -4887,7 +4929,7 @@ private static bool CanReuseParameter(CSharp.Syntax.ParameterSyntax parameter, b // We can only reuse parameters without identifiers (found in extension declarations) in context that allow optional identifiers. // The reverse is fine though. Normal parameters (from non extensions) can be re-used into an extension declaration // as all normal parameters are legal extension parameters. - if (!allowOptionalIdentifier && parameter.Identifier.Kind() == SyntaxKind.None) + if (!identifierIsOptional && parameter.Identifier.Kind() == SyntaxKind.None) { return false; } @@ -4897,9 +4939,9 @@ private static bool CanReuseParameter(CSharp.Syntax.ParameterSyntax parameter, b #nullable enable - private ParameterSyntax ParseParameter(bool allowOptionalIdentifier) + private ParameterSyntax ParseParameter(bool identifierIsOptional) { - if (this.IsIncrementalAndFactoryContextMatches && CanReuseParameter(this.CurrentNode as Syntax.ParameterSyntax, allowOptionalIdentifier)) + if (this.IsIncrementalAndFactoryContextMatches && CanReuseParameter(this.CurrentNode as Syntax.ParameterSyntax, identifierIsOptional)) { return (ParameterSyntax)this.EatNode(); } @@ -4922,12 +4964,12 @@ private ParameterSyntax ParseParameter(bool allowOptionalIdentifier) SyntaxToken? identifier; if (this.CurrentToken.Kind == SyntaxKind.IdentifierToken && IsCurrentTokenWhereOfConstraintClause()) { - identifier = allowOptionalIdentifier ? null : this.AddError(CreateMissingIdentifierToken(), ErrorCode.ERR_IdentifierExpected); + identifier = identifierIsOptional ? null : this.AddError(CreateMissingIdentifierToken(), ErrorCode.ERR_IdentifierExpected); } else { // The receiver parameter on an extension declaration may have a name or not - identifier = allowOptionalIdentifier && this.CurrentToken.Kind != SyntaxKind.IdentifierToken + identifier = identifierIsOptional && this.CurrentToken.Kind != SyntaxKind.IdentifierToken ? null : this.ParseIdentifierToken(); } @@ -5382,6 +5424,7 @@ private static SyntaxTokenList GetOriginalModifiers(CSharp.CSharpSyntaxNode decl return ((CSharp.Syntax.AccessorDeclarationSyntax)decl).Modifiers; case SyntaxKind.ClassDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.RecordStructDeclaration: @@ -5747,7 +5790,7 @@ private bool IsLocalFunctionAfterIdentifier() using var _ = this.GetDisposableResetPoint(resetOnDispose: true); var typeParameterListOpt = this.ParseTypeParameterList(); - var paramList = ParseParenthesizedParameterList(forExtension: false); + var paramList = ParseParenthesizedParameterList(forExtensionOrUnion: false); if (!paramList.IsMissing && (this.CurrentToken.Kind is SyntaxKind.OpenBraceToken or SyntaxKind.EqualsGreaterThanToken || @@ -5807,7 +5850,7 @@ private DelegateDeclarationSyntax ParseDelegateDeclaration(SyntaxList); if (this.CurrentToken.ContextualKind == SyntaxKind.WhereKeyword) @@ -8732,7 +8775,7 @@ private bool IsPossibleMethodDeclarationFollowingNullableType() var saveTerm = _termState; _termState |= TerminatorState.IsEndOfMethodSignature; - var paramList = this.ParseParenthesizedParameterList(forExtension: false); + var paramList = this.ParseParenthesizedParameterList(forExtensionOrUnion: false); _termState = saveTerm; var separatedParameters = paramList.Parameters.GetWithSeparators(); @@ -10949,7 +10992,7 @@ private LocalFunctionStatementSyntax TryParseLocalFunctionStatementBody( TypeParameterListSyntax typeParameterListOpt = this.ParseTypeParameterList(); // "await f()" still makes sense, so don't force accept a local function if there's a type parameter list. - ParameterListSyntax paramList = this.ParseParenthesizedParameterList(forExtension: false); + ParameterListSyntax paramList = this.ParseParenthesizedParameterList(forExtensionOrUnion: false); // "await x()" is ambiguous (see note at start of this method), but we assume "await x(await y)" is meant to be a function if it's in a non-async context. if (!forceLocalFunc) { @@ -13692,7 +13735,7 @@ AnonymousMethodExpressionSyntax parseAnonymousMethodExpressionWorker() ParameterListSyntax parameterList = null; if (this.CurrentToken.Kind == SyntaxKind.OpenParenToken) { - parameterList = this.ParseParenthesizedParameterList(forExtension: false); + parameterList = this.ParseParenthesizedParameterList(forExtensionOrUnion: false); } // In mismatched braces cases (missing a }) it is possible for delegate declarations to be diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Shipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Shipped.txt index de58f998845c..057a7b0504d6 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Shipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Shipped.txt @@ -256,6 +256,7 @@ Microsoft.CodeAnalysis.CSharp.DeconstructionInfo.Nested.get -> System.Collection Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.CurrentConversion.get -> Microsoft.CodeAnalysis.CSharp.Conversion Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.CurrentProperty.get -> Microsoft.CodeAnalysis.IPropertySymbol? +Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.DisposeAwaitableInfo.get -> Microsoft.CodeAnalysis.CSharp.AwaitExpressionInfo Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.DisposeMethod.get -> Microsoft.CodeAnalysis.IMethodSymbol? Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.ElementConversion.get -> Microsoft.CodeAnalysis.CSharp.Conversion Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.ElementType.get -> Microsoft.CodeAnalysis.ITypeSymbol? @@ -263,6 +264,7 @@ Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.Equals(Microsoft.CodeAnalysis Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.ForEachStatementInfo() -> void Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.GetEnumeratorMethod.get -> Microsoft.CodeAnalysis.IMethodSymbol? Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.IsAsynchronous.get -> bool +Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.MoveNextAwaitableInfo.get -> Microsoft.CodeAnalysis.CSharp.AwaitExpressionInfo Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.MoveNextMethod.get -> Microsoft.CodeAnalysis.IMethodSymbol? Microsoft.CodeAnalysis.CSharp.InterceptableLocation Microsoft.CodeAnalysis.CSharp.LanguageVersion @@ -4649,6 +4651,8 @@ static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.ClassifyConversion(this Mi static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.ClassifyConversion(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax! expression, Microsoft.CodeAnalysis.ITypeSymbol! destination, bool isExplicitInSource = false) -> Microsoft.CodeAnalysis.CSharp.Conversion static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetAliasInfo(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.IdentifierNameSyntax! nameSyntax, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.IAliasSymbol? static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetAwaitExpressionInfo(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.AwaitExpressionSyntax! awaitExpression) -> Microsoft.CodeAnalysis.CSharp.AwaitExpressionInfo +static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetAwaitExpressionInfo(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.LocalDeclarationStatementSyntax! awaitUsingDeclaration) -> Microsoft.CodeAnalysis.CSharp.AwaitExpressionInfo +static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetAwaitExpressionInfo(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.UsingStatementSyntax! awaitUsingStatement) -> Microsoft.CodeAnalysis.CSharp.AwaitExpressionInfo static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetCollectionInitializerSymbolInfo(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.SymbolInfo static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetCompilationUnitRoot(this Microsoft.CodeAnalysis.SyntaxTree! tree, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.CSharp.Syntax.CompilationUnitSyntax! static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetConstantValue(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionSyntax! expression, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Microsoft.CodeAnalysis.Optional diff --git a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt index 253ca47f335d..79a11544d739 100644 --- a/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/CSharp/Portable/PublicAPI.Unshipped.txt @@ -1,19 +1,20 @@ -Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.DisposeAwaitableInfo.get -> Microsoft.CodeAnalysis.CSharp.AwaitExpressionInfo -Microsoft.CodeAnalysis.CSharp.ForEachStatementInfo.MoveNextAwaitableInfo.get -> Microsoft.CodeAnalysis.CSharp.AwaitExpressionInfo -Microsoft.CodeAnalysis.CSharp.SyntaxKind.WithElement = 9081 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind -static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetAwaitExpressionInfo(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.LocalDeclarationStatementSyntax! awaitUsingDeclaration) -> Microsoft.CodeAnalysis.CSharp.AwaitExpressionInfo -static Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetAwaitExpressionInfo(this Microsoft.CodeAnalysis.SemanticModel? semanticModel, Microsoft.CodeAnalysis.CSharp.Syntax.UsingStatementSyntax! awaitUsingStatement) -> Microsoft.CodeAnalysis.CSharp.AwaitExpressionInfo -Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax -Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.AddArgumentListArguments(params Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentSyntax![]! items) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! -Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.ArgumentList.get -> Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax! -Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.Update(Microsoft.CodeAnalysis.SyntaxToken withKeyword, Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax! argumentList) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! -Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.WithArgumentList(Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax! argumentList) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! -Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.WithKeyword.get -> Microsoft.CodeAnalysis.SyntaxToken -Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.WithWithKeyword(Microsoft.CodeAnalysis.SyntaxToken withKeyword) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! -override Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter.VisitWithElement(Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! node) -> Microsoft.CodeAnalysis.SyntaxNode? -override Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.Accept(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor! visitor) -> void -override Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.Accept(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor! visitor) -> TResult? -static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.WithElement(Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax? argumentList = null) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! -static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.WithElement(Microsoft.CodeAnalysis.SyntaxToken withKeyword, Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax! argumentList) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! -virtual Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor.VisitWithElement(Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! node) -> void -virtual Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor.VisitWithElement(Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! node) -> TResult? +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.SyntaxKind.WithElement = 9081 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.AddArgumentListArguments(params Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentSyntax![]! items) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.ArgumentList.get -> Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax! +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.Update(Microsoft.CodeAnalysis.SyntaxToken withKeyword, Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax! argumentList) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.WithArgumentList(Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax! argumentList) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.WithKeyword.get -> Microsoft.CodeAnalysis.SyntaxToken +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.WithWithKeyword(Microsoft.CodeAnalysis.SyntaxToken withKeyword) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! +[RSEXPERIMENTAL006]override Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter.VisitWithElement(Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! node) -> Microsoft.CodeAnalysis.SyntaxNode? +[RSEXPERIMENTAL006]override Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.Accept(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor! visitor) -> void +[RSEXPERIMENTAL006]override Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax.Accept(Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor! visitor) -> TResult? +[RSEXPERIMENTAL006]static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.WithElement(Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax? argumentList = null) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! +[RSEXPERIMENTAL006]static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.WithElement(Microsoft.CodeAnalysis.SyntaxToken withKeyword, Microsoft.CodeAnalysis.CSharp.Syntax.ArgumentListSyntax! argumentList) -> Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! +[RSEXPERIMENTAL006]virtual Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor.VisitWithElement(Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! node) -> void +[RSEXPERIMENTAL006]virtual Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor.VisitWithElement(Microsoft.CodeAnalysis.CSharp.Syntax.WithElementSyntax! node) -> TResult? + +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.Conversion.IsUnion.get -> bool +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.SyntaxKind.UnionKeyword = 8452 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.CSharp.SyntaxKind.UnionDeclaration = 9082 -> Microsoft.CodeAnalysis.CSharp.SyntaxKind +[RSEXPERIMENTAL006]static Microsoft.CodeAnalysis.CSharp.SyntaxFactory.StructDeclaration(Microsoft.CodeAnalysis.CSharp.SyntaxKind kind, Microsoft.CodeAnalysis.SyntaxList attributeLists, Microsoft.CodeAnalysis.SyntaxTokenList modifiers, Microsoft.CodeAnalysis.SyntaxToken keyword, Microsoft.CodeAnalysis.SyntaxToken identifier, Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax? typeParameterList, Microsoft.CodeAnalysis.CSharp.Syntax.ParameterListSyntax? parameterList, Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax? baseList, Microsoft.CodeAnalysis.SyntaxList constraintClauses, Microsoft.CodeAnalysis.SyntaxToken openBraceToken, Microsoft.CodeAnalysis.SyntaxList members, Microsoft.CodeAnalysis.SyntaxToken closeBraceToken, Microsoft.CodeAnalysis.SyntaxToken semicolonToken) -> Microsoft.CodeAnalysis.CSharp.Syntax.StructDeclarationSyntax! diff --git a/src/Compilers/CSharp/Portable/Symbols/AliasSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/AliasSymbol.cs index aa96d2b54b2d..805c2c524561 100644 --- a/src/Compilers/CSharp/Portable/Symbols/AliasSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/AliasSymbol.cs @@ -189,6 +189,8 @@ internal sealed override ObsoleteAttributeData? ObsoleteAttributeData get { return null; } } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + public override Accessibility DeclaredAccessibility { get diff --git a/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/PublicSymbols/AnonymousManager.TypeOrDelegatePublicSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/PublicSymbols/AnonymousManager.TypeOrDelegatePublicSymbol.cs index 3b8256bc5fb5..dc45932cccc9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/PublicSymbols/AnonymousManager.TypeOrDelegatePublicSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/PublicSymbols/AnonymousManager.TypeOrDelegatePublicSymbol.cs @@ -45,6 +45,8 @@ internal sealed override IEnumerable GetFieldsToEmit() internal sealed override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal sealed override bool IsInterpolatedStringHandlerType => false; internal sealed override ParameterSymbol? ExtensionParameter => null; @@ -126,7 +128,7 @@ public sealed override bool IsSealed get { return true; } } - public sealed override bool MightContainExtensionMethods + public sealed override bool MightContainExtensions { get { return false; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/SynthesizedSymbols/AnonymousType.PropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/SynthesizedSymbols/AnonymousType.PropertySymbol.cs index 0caa26f8424d..e8d7b1272601 100644 --- a/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/SynthesizedSymbols/AnonymousType.PropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/SynthesizedSymbols/AnonymousType.PropertySymbol.cs @@ -133,6 +133,8 @@ public override bool IsAbstract internal sealed override bool HasUnscopedRefAttribute => false; + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal sealed override ObsoleteAttributeData ObsoleteAttributeData { get { return null; } diff --git a/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/SynthesizedSymbols/AnonymousType.TypeOrDelegateTemplateSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/SynthesizedSymbols/AnonymousType.TypeOrDelegateTemplateSymbol.cs index ae62b4c0cc43..06c2e1de728d 100644 --- a/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/SynthesizedSymbols/AnonymousType.TypeOrDelegateTemplateSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/AnonymousTypes/SynthesizedSymbols/AnonymousType.TypeOrDelegateTemplateSymbol.cs @@ -123,6 +123,8 @@ internal override bool GetGuidString(out string? guidString) internal sealed override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal sealed override bool IsInterpolatedStringHandlerType => false; internal sealed override ParameterSymbol? ExtensionParameter => null; @@ -181,7 +183,7 @@ public sealed override bool IsSealed get { return true; } } - public sealed override bool MightContainExtensionMethods + public sealed override bool MightContainExtensions { get { return false; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/AssemblySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/AssemblySymbol.cs index cdc74379a001..404848aaa54c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/AssemblySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/AssemblySymbol.cs @@ -310,6 +310,8 @@ public sealed override Symbol ContainingSymbol } } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal abstract bool HasImportedFromTypeLibAttribute { get; } internal abstract bool HasPrimaryInteropAssemblyAttribute { get; } @@ -614,13 +616,13 @@ bool IAssemblySymbolInternal.AreInternalsVisibleToThisAssembly(IAssemblySymbolIn public abstract ICollection NamespaceNames { get; } /// - /// Returns true if this assembly might contain extension methods. If this property - /// returns false, there are no extension methods in this assembly. + /// Returns true if this assembly might contain extension members or methods. If this property + /// returns false, there are no extension members or methods in this assembly. /// /// - /// This property allows the search for extension methods to be narrowed quickly. + /// This property allows the search for extension members or methods to be narrowed quickly. /// - public abstract bool MightContainExtensionMethods { get; } + public abstract bool MightContainExtensions { get; } /// /// Gets the symbol for the pre-defined type from core library associated with this assembly. diff --git a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs index c9410bc2f831..ba52bb2f4c1d 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/MethodWellKnownAttributeData.cs @@ -60,6 +60,22 @@ public bool HasUnscopedRefAttribute } } + private bool _hasRequiresUnsafeAttribute; + public bool HasRequiresUnsafeAttribute + { + get + { + VerifySealed(expected: true); + return _hasRequiresUnsafeAttribute; + } + set + { + VerifySealed(expected: false); + _hasRequiresUnsafeAttribute = value; + SetDataStored(); + } + } + private ImmutableArray _memberNotNullAttributeData = ImmutableArray.Empty; public void AddNotNullMember(string memberName) diff --git a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/PropertyWellKnownAttributeData.cs b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/PropertyWellKnownAttributeData.cs index 8993a1d8ff8f..266bab4d3ef9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/PropertyWellKnownAttributeData.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/PropertyWellKnownAttributeData.cs @@ -110,6 +110,22 @@ public bool HasUnscopedRefAttribute } } + private bool _hasRequiresUnsafeAttribute; + public bool HasRequiresUnsafeAttribute + { + get + { + VerifySealed(expected: true); + return _hasRequiresUnsafeAttribute; + } + set + { + VerifySealed(expected: false); + _hasRequiresUnsafeAttribute = value; + SetDataStored(); + } + } + private ImmutableArray _memberNotNullAttributeData = ImmutableArray.Empty; public void AddNotNullMember(string memberName) diff --git a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/TypeWellKnownEarlyAttributeData.cs b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/TypeWellKnownEarlyAttributeData.cs index d74bd9145a68..f085a0078a8f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/TypeWellKnownEarlyAttributeData.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Attributes/WellKnownAttributeData/TypeWellKnownEarlyAttributeData.cs @@ -86,5 +86,23 @@ public CollectionBuilderAttributeData? CollectionBuilder } } #endregion + + #region UnionAttribute + private bool _hasUnionAttribute; + public bool HasUnionAttribute + { + get + { + VerifySealed(expected: true); + return _hasUnionAttribute; + } + set + { + VerifySealed(expected: false); + _hasUnionAttribute = value; + SetDataStored(); + } + } + #endregion } } diff --git a/src/Compilers/CSharp/Portable/Symbols/CallerUnsafeMode.cs b/src/Compilers/CSharp/Portable/Symbols/CallerUnsafeMode.cs new file mode 100644 index 000000000000..bbda22cf9004 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/CallerUnsafeMode.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.CSharp.Symbols; + +namespace Microsoft.CodeAnalysis.CSharp; + +/// +/// Member safety under updated memory safety rules (). +/// +internal enum CallerUnsafeMode +{ + /// + /// The member is not considered unsafe under the updated memory safety rules. + /// + None, + + /// + /// The member is implicitly considered unsafe because it contains pointers in its signature. + /// + Implicit, + + /// + /// The member is explicitly marked with or under the updated memory safety rules. + /// + Explicit, +} diff --git a/src/Compilers/CSharp/Portable/Symbols/Compilation_WellKnownMembers.cs b/src/Compilers/CSharp/Portable/Symbols/Compilation_WellKnownMembers.cs index a4f9c024f661..53d736ede853 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Compilation_WellKnownMembers.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Compilation_WellKnownMembers.cs @@ -541,6 +541,11 @@ internal void EnsureRequiresLocationAttributeExists(BindingDiagnosticBag? diagno EnsureEmbeddableAttributeExists(EmbeddableAttributes.RequiresLocationAttribute, diagnostics, location, modifyCompilation); } + internal void EnsureMemorySafetyRulesAttributeExists(BindingDiagnosticBag? diagnostics, Location location, bool modifyCompilation) + { + EnsureEmbeddableAttributeExists(EmbeddableAttributes.MemorySafetyRulesAttribute, diagnostics, location, modifyCompilation); + } + internal void EnsureParamCollectionAttributeExists(BindingDiagnosticBag? diagnostics, Location location, bool modifyCompilation) { EnsureEmbeddableAttributeExists(EmbeddableAttributes.ParamCollectionAttribute, diagnostics, location, modifyCompilation: modifyCompilation); @@ -654,6 +659,13 @@ internal bool CheckIfAttributeShouldBeEmbedded(EmbeddableAttributes attribute, B WellKnownType.System_Runtime_CompilerServices_RefSafetyRulesAttribute, WellKnownMember.System_Runtime_CompilerServices_RefSafetyRulesAttribute__ctor); + case EmbeddableAttributes.MemorySafetyRulesAttribute: + return CheckIfAttributeShouldBeEmbedded( + diagnosticsOpt, + locationOpt, + WellKnownType.System_Runtime_CompilerServices_MemorySafetyRulesAttribute, + WellKnownMember.System_Runtime_CompilerServices_MemorySafetyRulesAttribute__ctor); + case EmbeddableAttributes.RequiresLocationAttribute: return CheckIfAttributeShouldBeEmbedded( diagnosticsOpt, diff --git a/src/Compilers/CSharp/Portable/Symbols/DiscardSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/DiscardSymbol.cs index 08da8b9a72f1..de0c5c927c3b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/DiscardSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/DiscardSymbol.cs @@ -31,6 +31,7 @@ public DiscardSymbol(TypeWithAnnotations typeWithAnnotations) public override SymbolKind Kind => SymbolKind.Discard; public override ImmutableArray Locations => ImmutableArray.Empty; internal override ObsoleteAttributeData? ObsoleteAttributeData => null; + internal override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; internal override TResult Accept(CSharpSymbolVisitor visitor, TArgument a) => visitor.VisitDiscard(this, a); public override void Accept(CSharpSymbolVisitor visitor) => visitor.VisitDiscard(this); public override TResult Accept(CSharpSymbolVisitor visitor) => visitor.VisitDiscard(this); diff --git a/src/Compilers/CSharp/Portable/Symbols/EmbeddableAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/EmbeddableAttributes.cs index 2ce673078221..7e8809b60f2e 100644 --- a/src/Compilers/CSharp/Portable/Symbols/EmbeddableAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/EmbeddableAttributes.cs @@ -21,5 +21,6 @@ internal enum EmbeddableAttributes RequiresLocationAttribute = 0x200, ParamCollectionAttribute = 0x400, ExtensionMarkerAttribute = 0x800, + MemorySafetyRulesAttribute = 0x1000, } } diff --git a/src/Compilers/CSharp/Portable/Symbols/EnumConversions.cs b/src/Compilers/CSharp/Portable/Symbols/EnumConversions.cs index 0d40d07e8360..810d9fc80c70 100644 --- a/src/Compilers/CSharp/Portable/Symbols/EnumConversions.cs +++ b/src/Compilers/CSharp/Portable/Symbols/EnumConversions.cs @@ -33,6 +33,7 @@ internal static TypeKind ToTypeKind(this DeclarationKind kind) return TypeKind.Interface; case DeclarationKind.Struct: + case DeclarationKind.Union: case DeclarationKind.RecordStruct: return TypeKind.Struct; diff --git a/src/Compilers/CSharp/Portable/Symbols/ErrorMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ErrorMethodSymbol.cs index 438004549e8c..7a9d7fad8236 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ErrorMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ErrorMethodSymbol.cs @@ -178,6 +178,8 @@ public override bool ReturnsVoid public override FlowAnalysisAnnotations FlowAnalysisAnnotations => FlowAnalysisAnnotations.None; + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => ThreeState.Unknown; + public override bool IsVararg { get { return false; } @@ -291,6 +293,8 @@ protected override bool HasSetsRequiredMembersImpl internal sealed override bool UseUpdatedEscapeRules => false; + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal sealed override bool HasAsyncMethodBuilderAttribute(out TypeSymbol builderArgument) { builderArgument = null; diff --git a/src/Compilers/CSharp/Portable/Symbols/ErrorPropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ErrorPropertySymbol.cs index 3662cefe70bb..67f5ae592fcf 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ErrorPropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ErrorPropertySymbol.cs @@ -81,6 +81,8 @@ public ErrorPropertySymbol(Symbol containingSymbol, TypeSymbol type, string name internal sealed override bool HasUnscopedRefAttribute => false; + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal sealed override ObsoleteAttributeData ObsoleteAttributeData { get { return null; } } public override ImmutableArray Parameters { get { return ImmutableArray.Empty; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/ErrorTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ErrorTypeSymbol.cs index c5ebf783d86d..f557c462aa26 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ErrorTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ErrorTypeSymbol.cs @@ -432,7 +432,7 @@ internal sealed override bool HasSpecialName get { return false; } } - public sealed override bool MightContainExtensionMethods + public sealed override bool MightContainExtensions { get { @@ -452,6 +452,8 @@ internal override bool GetGuidString(out string? guidString) internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal override bool IsInterpolatedStringHandlerType => false; internal override ImmutableArray InterfacesNoUseSiteDiagnostics(ConsList? basesBeingResolved) diff --git a/src/Compilers/CSharp/Portable/Symbols/Extensions/RewrittenMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Extensions/RewrittenMethodSymbol.cs index 504e348e8514..be787c0bb645 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Extensions/RewrittenMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Extensions/RewrittenMethodSymbol.cs @@ -64,6 +64,10 @@ internal override bool IsIterator get { return _originalMethod.IsIterator; } } + public sealed override bool IsAsync => _originalMethod.IsAsync; + + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => _originalMethod.RuntimeAsyncMethodGenerationAttributeSetting; + internal sealed override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree localTree) { return _originalMethod.CalculateLocalSyntaxOffset(localPosition, localTree); diff --git a/src/Compilers/CSharp/Portable/Symbols/Extensions/SourceExtensionImplementationMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Extensions/SourceExtensionImplementationMethodSymbol.cs index 43cc899b2162..558852e91956 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Extensions/SourceExtensionImplementationMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Extensions/SourceExtensionImplementationMethodSymbol.cs @@ -65,12 +65,13 @@ internal override int ParameterCount internal sealed override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, ref ArrayBuilder attributes) { - // Copy ORPA from the property onto the implementation accessors + // Copy some attributes from the property onto the implementation accessors if (_originalMethod is SourcePropertyAccessorSymbol { AssociatedSymbol: SourcePropertySymbolBase extensionProperty }) { foreach (CSharpAttributeData attr in extensionProperty.GetAttributes()) { - if (attr.IsTargetAttribute(AttributeDescription.OverloadResolutionPriorityAttribute)) + if (attr.IsTargetAttribute(AttributeDescription.OverloadResolutionPriorityAttribute) || + attr.IsTargetAttribute(AttributeDescription.RequiresUnsafeAttribute)) { AddSynthesizedAttribute(ref attributes, attr); } diff --git a/src/Compilers/CSharp/Portable/Symbols/FieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/FieldSymbol.cs index 341314865036..840562be89c1 100644 --- a/src/Compilers/CSharp/Portable/Symbols/FieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/FieldSymbol.cs @@ -336,6 +336,8 @@ internal virtual FieldSymbol AsMember(NamedTypeSymbol newOwner) /// internal abstract bool IsRequired { get; } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; // https://github.com/dotnet/roslyn/issues/82546: Support unsafe fields? + #region Use-Site Diagnostics internal override UseSiteInfo GetUseSiteInfo() diff --git a/src/Compilers/CSharp/Portable/Symbols/FunctionPointers/FunctionPointerMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/FunctionPointers/FunctionPointerMethodSymbol.cs index 6b17d777b376..38310a8aacd0 100644 --- a/src/Compilers/CSharp/Portable/Symbols/FunctionPointers/FunctionPointerMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/FunctionPointers/FunctionPointerMethodSymbol.cs @@ -845,6 +845,7 @@ public override bool IsVararg public override FlowAnalysisAnnotations ReturnTypeFlowAnalysisAnnotations => FlowAnalysisAnnotations.None; public override ImmutableHashSet ReturnNotNullIfParameterNotNull => ImmutableHashSet.Empty; public override FlowAnalysisAnnotations FlowAnalysisAnnotations => FlowAnalysisAnnotations.None; + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => false; internal override bool IsMetadataVirtual(IsMetadataVirtualOption option = IsMetadataVirtualOption.None) => false; internal sealed override UnmanagedCallersOnlyAttributeData? GetUnmanagedCallersOnlyAttributeData(bool forceComplete) => null; @@ -861,6 +862,10 @@ public override bool IsVararg protected sealed override bool HasSetsRequiredMembersImpl => throw ExceptionUtilities.Unreachable(); internal sealed override bool HasUnscopedRefAttribute => false; + // The function pointer type itself is not unsafe under the new rules, only its invocation is. + // That is analogous to normal pointer types (and pointer dereference, respectively) under the new rules. + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal sealed override bool HasAsyncMethodBuilderAttribute(out TypeSymbol? builderArgument) { builderArgument = null; diff --git a/src/Compilers/CSharp/Portable/Symbols/LabelSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/LabelSymbol.cs index 8f65bacb4c36..ce59b73eca18 100644 --- a/src/Compilers/CSharp/Portable/Symbols/LabelSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/LabelSymbol.cs @@ -90,6 +90,8 @@ internal sealed override ObsoleteAttributeData? ObsoleteAttributeData get { return null; } } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + /// /// Returns 'NotApplicable' because label can't be used outside the member body. /// diff --git a/src/Compilers/CSharp/Portable/Symbols/LocalSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/LocalSymbol.cs index 1de47b96091f..f56ae464684b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/LocalSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/LocalSymbol.cs @@ -379,6 +379,8 @@ public abstract RefKind RefKind get; } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + protected sealed override ISymbol CreateISymbol() { return new PublicModel.LocalSymbol(this); diff --git a/src/Compilers/CSharp/Portable/Symbols/MemberSymbolExtensions.cs b/src/Compilers/CSharp/Portable/Symbols/MemberSymbolExtensions.cs index 623c378765b8..17f7017cd892 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MemberSymbolExtensions.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MemberSymbolExtensions.cs @@ -361,6 +361,12 @@ internal static bool HasParameterContainingPointerType(this Symbol member) } } + if (member.TryGetInstanceExtensionParameter(out var extensionParameter) && + extensionParameter.Type.ContainsPointerOrFunctionPointer()) + { + return true; + } + return false; } diff --git a/src/Compilers/CSharp/Portable/Symbols/MergedNamespaceSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/MergedNamespaceSymbol.cs index 78bef38de8be..5f3aaa68a556 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MergedNamespaceSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MergedNamespaceSymbol.cs @@ -290,25 +290,12 @@ public override ImmutableArray DeclaringSyntaxReferences } } - internal override void GetExtensionMethods(ArrayBuilder methods, string name, int arity, LookupOptions options) - { - foreach (NamespaceSymbol namespaceSymbol in _namespacesToMerge) - { - namespaceSymbol.GetExtensionMethods(methods, name, arity, options); - } - } - #nullable enable - // Overridden to avoid NamespaceSymbol.GetExtensionContainers call to GetTypeMembersUnordered. The combination of the - // CreateRange and OfType Linq calls in MergedNamespaceSymbol.GetTypeMembersUnordered causes a full array allocation. - internal sealed override void GetExtensionMembers(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, ConsList fieldsBeingBound) + internal sealed override void GetAllExtensionMembers(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, ConsList fieldsBeingBound) { - foreach (var member in GetMembersUnordered()) + foreach (NamespaceSymbol namespaceSymbol in _namespacesToMerge) { - if (member is NamedTypeSymbol type) - { - type.GetExtensionMembers(members, name, alternativeName, arity, options, fieldsBeingBound); - } + namespaceSymbol.GetAllExtensionMembers(members, name, alternativeName, arity, options, fieldsBeingBound); } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEAssemblySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEAssemblySymbol.cs index 8c572aa6a4bd..f830ae012238 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEAssemblySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEAssemblySymbol.cs @@ -154,11 +154,11 @@ ImmutableArray loadAndFilterAttributes() return []; } - var mightContainExtensionMethods = this.MightContainExtensionMethods; + var mightContainExtensions = this.MightContainExtensions; using var builder = TemporaryArray.Empty; foreach (var handle in customAttributeHandles) { - if (mightContainExtensionMethods && containingModule.AttributeMatchesFilter(handle, AttributeDescription.CaseSensitiveExtensionAttribute)) + if (mightContainExtensions && containingModule.AttributeMatchesFilter(handle, AttributeDescription.CaseSensitiveExtensionAttribute)) continue; builder.Add(new PEAttributeData(containingModule, handle)); @@ -289,7 +289,7 @@ internal override bool IsLinked } } - public override bool MightContainExtensionMethods + public override bool MightContainExtensions { get { diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEEventSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEEventSymbol.cs index 741a8460157c..c9923800edee 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEEventSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEEventSymbol.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.CSharp.DocumentationComments; using Microsoft.CodeAnalysis.CSharp.Emit; using Microsoft.CodeAnalysis.PooledObjects; @@ -40,6 +41,8 @@ internal sealed class PEEventSymbol : EventSymbol private const int UnsetAccessibility = -1; private int _lazyDeclaredAccessibility = UnsetAccessibility; + private byte _lazyRequiresUnsafe; + private readonly Flags _flags; [Flags] private enum Flags : byte @@ -362,9 +365,47 @@ public override ImmutableArray GetAttributes() if (_lazyCustomAttributes.IsDefault) { var containingPEModuleSymbol = (PEModuleSymbol)this.ContainingModule; - containingPEModuleSymbol.LoadCustomAttributes(_handle, ref _lazyCustomAttributes); + + var requiresUnsafeState = (ThreeState)Volatile.Read(ref _lazyRequiresUnsafe); + bool checkForRequiresUnsafe = requiresUnsafeState != ThreeState.False; + + if (checkForRequiresUnsafe) + { + ImmutableArray attributes = loadAndFilterAttributes(containingPEModuleSymbol, out var hasRequiresUnsafeAttribute); + + _lazyRequiresUnsafe = (byte)ComputeRequiresUnsafe(hasRequiresUnsafeAttribute).ToThreeState(); + + ImmutableInterlocked.InterlockedInitialize(ref _lazyCustomAttributes, attributes); + } + else + { + containingPEModuleSymbol.LoadCustomAttributes(_handle, ref _lazyCustomAttributes); + } } return _lazyCustomAttributes; + + ImmutableArray loadAndFilterAttributes(PEModuleSymbol containingModule, out bool hasRequiresUnsafeAttribute) + { + hasRequiresUnsafeAttribute = false; + + if (!containingModule.TryGetNonEmptyCustomAttributes(_handle, out var customAttributeHandles)) + { + return []; + } + + using var builder = TemporaryArray.Empty; + foreach (var handle in customAttributeHandles) + { + if (containingModule.AttributeMatchesFilter(handle, AttributeDescription.RequiresUnsafeAttribute)) + { + hasRequiresUnsafeAttribute = true; + } + + builder.Add(new PEAttributeData(containingModule, handle)); + } + + return builder.ToImmutableAndClear(); + } } internal override IEnumerable GetCustomAttributesToEmit(PEModuleBuilder moduleBuilder) @@ -503,6 +544,47 @@ internal override ObsoleteAttributeData ObsoleteAttributeData } } + private bool RequiresUnsafe + { + get + { + var requiresUnsafeState = (ThreeState)Volatile.Read(ref _lazyRequiresUnsafe); + if (!requiresUnsafeState.HasValue()) + { + var containingPEModuleSymbol = (PEModuleSymbol)this.ContainingModule; + bool hasRequiresUnsafeAttribute = containingPEModuleSymbol.Module.HasAttribute(_handle, AttributeDescription.RequiresUnsafeAttribute); + bool requiresUnsafe = ComputeRequiresUnsafe(hasRequiresUnsafeAttribute); + _lazyRequiresUnsafe = (byte)requiresUnsafe.ToThreeState(); + return requiresUnsafe; + } + + return requiresUnsafeState.Value(); + } + } + + private bool ComputeRequiresUnsafe(bool hasRequiresUnsafeAttribute) + { + return ContainingModule.UseUpdatedMemorySafetyRules + ? hasRequiresUnsafeAttribute + // This might be expensive, so we cache it in flags. + : Type.ContainsPointerOrFunctionPointer(); + } + + internal override CallerUnsafeMode CallerUnsafeMode + { + get + { + if (!RequiresUnsafe) + { + return CallerUnsafeMode.None; + } + + return ContainingModule.UseUpdatedMemorySafetyRules + ? CallerUnsafeMode.Explicit + : CallerUnsafeMode.Implicit; + } + } + internal sealed override CSharpCompilation? DeclaringCompilation // perf, not correctness { get { return null; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs index 47993dadb6ee..6b23007a94b8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEMethodSymbol.cs @@ -46,9 +46,9 @@ public SignatureData(SignatureHeader header, ImmutableArray par // This type is used to compact many different bits of information efficiently. private struct PackedFlags { - // We currently pack everything into a 32-bit int with the following layout: + // We currently pack everything into a 64-bit long with the following layout: // - // |y|x|w|v|u|t|s|r|q|p|ooo|n|m|l|k|j|i|h|g|f|e|d|c|b|aaaaa| + // |..............................|b'|a'|z|y|x|w|v|u|t|s|r|q|p|ooo|n|m|l|k|j|i|h|g|f|e|d|c|b|aaaaa| // // a = method kind. 5 bits. // b = method kind populated. 1 bit. @@ -78,39 +78,44 @@ private struct PackedFlags // x = IsUnscopedRef. 1 bit. // y = IsUnscopedRefPopulated. 1 bit. // z = OverloadResolutionPriorityPopulated. 1 bit. - // 1 bits remain for future purposes. + // a' = RequiresUnsafe. 1 bit. + // b' = RequiresUnsafePopulated. 1 bit. + // 30 bits remain for future purposes. private const int MethodKindOffset = 0; - private const int MethodKindMask = 0x1F; - - private const int MethodKindIsPopulatedBit = 0x1 << 5; - private const int IsExtensionMethodBit = 0x1 << 6; - private const int IsExtensionMethodIsPopulatedBit = 0x1 << 7; - private const int IsExplicitFinalizerOverrideBit = 0x1 << 8; - private const int IsExplicitClassOverrideBit = 0x1 << 9; - private const int IsExplicitOverrideIsPopulatedBit = 0x1 << 10; - private const int IsObsoleteAttributePopulatedBit = 0x1 << 11; - private const int IsCustomAttributesPopulatedBit = 0x1 << 12; - private const int IsUseSiteDiagnosticPopulatedBit = 0x1 << 13; - private const int IsConditionalPopulatedBit = 0x1 << 14; - private const int IsOverriddenOrHiddenMembersPopulatedBit = 0x1 << 15; - private const int IsReadOnlyBit = 0x1 << 16; - private const int IsReadOnlyPopulatedBit = 0x1 << 17; + private const long MethodKindMask = 0x1F; + + private const long B = 1; + private const long MethodKindIsPopulatedBit = B << 5; + private const long IsExtensionMethodBit = B << 6; + private const long IsExtensionMethodIsPopulatedBit = B << 7; + private const long IsExplicitFinalizerOverrideBit = B << 8; + private const long IsExplicitClassOverrideBit = B << 9; + private const long IsExplicitOverrideIsPopulatedBit = B << 10; + private const long IsObsoleteAttributePopulatedBit = B << 11; + private const long IsCustomAttributesPopulatedBit = B << 12; + private const long IsUseSiteDiagnosticPopulatedBit = B << 13; + private const long IsConditionalPopulatedBit = B << 14; + private const long IsOverriddenOrHiddenMembersPopulatedBit = B << 15; + private const long IsReadOnlyBit = B << 16; + private const long IsReadOnlyPopulatedBit = B << 17; private const int NullableContextOffset = 18; - private const int NullableContextMask = 0x7; - private const int DoesNotReturnBit = 0x1 << 21; - private const int IsDoesNotReturnPopulatedBit = 0x1 << 22; - private const int IsMemberNotNullPopulatedBit = 0x1 << 23; - private const int IsInitOnlyBit = 0x1 << 24; - private const int IsInitOnlyPopulatedBit = 0x1 << 25; - private const int IsUnmanagedCallersOnlyAttributePopulatedBit = 0x1 << 26; - private const int HasSetsRequiredMembersBit = 0x1 << 27; - private const int HasSetsRequiredMembersPopulatedBit = 0x1 << 28; - private const int IsUnscopedRefBit = 0x1 << 29; - private const int IsUnscopedRefPopulatedBit = 0x1 << 30; - private const int OverloadResolutionPriorityPopulatedBit = 0x1 << 31; - - private int _bits; + private const long NullableContextMask = 0x7; + private const long DoesNotReturnBit = B << 21; + private const long IsDoesNotReturnPopulatedBit = B << 22; + private const long IsMemberNotNullPopulatedBit = B << 23; + private const long IsInitOnlyBit = B << 24; + private const long IsInitOnlyPopulatedBit = B << 25; + private const long IsUnmanagedCallersOnlyAttributePopulatedBit = B << 26; + private const long HasSetsRequiredMembersBit = B << 27; + private const long HasSetsRequiredMembersPopulatedBit = B << 28; + private const long IsUnscopedRefBit = B << 29; + private const long IsUnscopedRefPopulatedBit = B << 30; + private const long OverloadResolutionPriorityPopulatedBit = B << 31; + private const long RequiresUnsafeBit = B << 32; + private const long RequiresUnsafePopulatedBit = B << 33; + + private long _bits; public MethodKind MethodKind { @@ -121,8 +126,8 @@ public MethodKind MethodKind set { - Debug.Assert((int)value == ((int)value & MethodKindMask)); - _bits = (_bits & ~(MethodKindMask << MethodKindOffset)) | (((int)value & MethodKindMask) << MethodKindOffset) | MethodKindIsPopulatedBit; + Debug.Assert((long)value == ((long)value & MethodKindMask)); + _bits = (_bits & ~(MethodKindMask << MethodKindOffset)) | (((long)value & MethodKindMask) << MethodKindOffset) | MethodKindIsPopulatedBit; } } @@ -150,46 +155,48 @@ public MethodKind MethodKind public bool IsUnscopedRef => (_bits & IsUnscopedRefBit) != 0; public bool IsUnscopedRefPopulated => (_bits & IsUnscopedRefPopulatedBit) != 0; public bool IsOverloadResolutionPriorityPopulated => (Volatile.Read(ref _bits) & OverloadResolutionPriorityPopulatedBit) != 0; + public bool RequiresUnsafe => (Volatile.Read(ref _bits) & RequiresUnsafeBit) != 0; + public bool RequiresUnsafePopulated => (Volatile.Read(ref _bits) & RequiresUnsafePopulatedBit) != 0; #if DEBUG static PackedFlags() { // Verify masks are sufficient for values. - Debug.Assert(EnumUtilities.ContainsAllValues(MethodKindMask)); - Debug.Assert(EnumUtilities.ContainsAllValues(NullableContextMask)); + Debug.Assert(EnumUtilities.ContainsAllValues((int)MethodKindMask)); + Debug.Assert(EnumUtilities.ContainsAllValues((int)NullableContextMask)); } #endif - private static bool BitsAreUnsetOrSame(int bits, int mask) + private static bool BitsAreUnsetOrSame(long bits, long mask) { return (bits & mask) == 0 || (bits & mask) == mask; } public void InitializeIsExtensionMethod(bool isExtensionMethod) { - int bitsToSet = (isExtensionMethod ? IsExtensionMethodBit : 0) | IsExtensionMethodIsPopulatedBit; + long bitsToSet = (isExtensionMethod ? IsExtensionMethodBit : 0) | IsExtensionMethodIsPopulatedBit; Debug.Assert(BitsAreUnsetOrSame(_bits, bitsToSet)); ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); } public void InitializeIsReadOnly(bool isReadOnly) { - int bitsToSet = (isReadOnly ? IsReadOnlyBit : 0) | IsReadOnlyPopulatedBit; + long bitsToSet = (isReadOnly ? IsReadOnlyBit : 0) | IsReadOnlyPopulatedBit; Debug.Assert(BitsAreUnsetOrSame(_bits, bitsToSet)); ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); } public void InitializeMethodKind(MethodKind methodKind) { - Debug.Assert((int)methodKind == ((int)methodKind & MethodKindMask)); - int bitsToSet = (((int)methodKind & MethodKindMask) << MethodKindOffset) | MethodKindIsPopulatedBit; + Debug.Assert((long)methodKind == ((long)methodKind & MethodKindMask)); + long bitsToSet = (((long)methodKind & MethodKindMask) << MethodKindOffset) | MethodKindIsPopulatedBit; Debug.Assert(BitsAreUnsetOrSame(_bits, bitsToSet)); ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); } public void InitializeIsExplicitOverride(bool isExplicitFinalizerOverride, bool isExplicitClassOverride) { - int bitsToSet = + long bitsToSet = (isExplicitFinalizerOverride ? IsExplicitFinalizerOverrideBit : 0) | (isExplicitClassOverride ? IsExplicitClassOverrideBit : 0) | IsExplicitOverrideIsPopulatedBit; @@ -229,12 +236,12 @@ public bool TryGetNullableContext(out byte? value) public bool SetNullableContext(byte? value) { - return ThreadSafeFlagOperations.Set(ref _bits, (((int)value.ToNullableContextFlags() & NullableContextMask) << NullableContextOffset)); + return ThreadSafeFlagOperations.Set(ref _bits, (((long)value.ToNullableContextFlags() & NullableContextMask) << NullableContextOffset)); } public bool InitializeDoesNotReturn(bool value) { - int bitsToSet = IsDoesNotReturnPopulatedBit; + long bitsToSet = IsDoesNotReturnPopulatedBit; if (value) bitsToSet |= DoesNotReturnBit; return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); @@ -247,7 +254,7 @@ public void SetIsMemberNotNullPopulated() public void InitializeIsInitOnly(bool isInitOnly) { - int bitsToSet = (isInitOnly ? IsInitOnlyBit : 0) | IsInitOnlyPopulatedBit; + long bitsToSet = (isInitOnly ? IsInitOnlyBit : 0) | IsInitOnlyPopulatedBit; Debug.Assert(BitsAreUnsetOrSame(_bits, bitsToSet)); ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); } @@ -259,7 +266,7 @@ public void SetIsUnmanagedCallersOnlyAttributePopulated() public bool InitializeSetsRequiredMembersBit(bool value) { - int bitsToSet = HasSetsRequiredMembersPopulatedBit; + long bitsToSet = HasSetsRequiredMembersPopulatedBit; if (value) bitsToSet |= HasSetsRequiredMembersBit; return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); @@ -267,7 +274,7 @@ public bool InitializeSetsRequiredMembersBit(bool value) public bool InitializeIsUnscopedRef(bool value) { - int bitsToSet = IsUnscopedRefPopulatedBit; + long bitsToSet = IsUnscopedRefPopulatedBit; if (value) bitsToSet |= IsUnscopedRefBit; return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); @@ -277,6 +284,14 @@ public void SetIsOverloadResolutionPriorityPopulated() { ThreadSafeFlagOperations.Set(ref _bits, OverloadResolutionPriorityPopulatedBit); } + + public bool InitializeRequiresUnsafe(bool value) + { + long bitsToSet = RequiresUnsafePopulatedBit; + if (value) bitsToSet |= RequiresUnsafeBit; + + return ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); + } } /// @@ -687,6 +702,15 @@ public override FlowAnalysisAnnotations FlowAnalysisAnnotations } } + internal override ThreeState RuntimeAsyncMethodGenerationAttributeSetting + { + get + { + Debug.Fail("Not expecting to get here; if we end up here through ENC, add tests to verify"); + return ThreeState.Unknown; + } + } + internal override ImmutableArray NotNullMembers { get @@ -978,7 +1002,7 @@ public override bool IsExtensionMethod { bool isExtensionMethod = false; if (this.MethodKind == MethodKind.Ordinary && IsValidExtensionMethodSignature() - && this.ContainingType.MightContainExtensionMethods) + && this.ContainingType.MightContainExtensions) { var moduleSymbol = _containingType.ContainingPEModule; isExtensionMethod = moduleSymbol.Module.HasExtensionAttribute(_handle, ignoreCase: false); @@ -997,9 +1021,10 @@ public override ImmutableArray GetAttributes() { if (!_packedFlags.IsCustomAttributesPopulated) { - var attributeData = loadAndFilterAttributes(out var isExtensionMethod, out var isReadOnly); + var attributeData = loadAndFilterAttributes(out var isExtensionMethod, out var isReadOnly, out var hasRequiresUnsafeAttribute); _packedFlags.InitializeIsExtensionMethod(isExtensionMethod); _packedFlags.InitializeIsReadOnly(isReadOnly); + _packedFlags.InitializeRequiresUnsafe(ComputeRequiresUnsafe(hasRequiresUnsafeAttribute)); // Store the result in uncommon fields only if it's not empty. Debug.Assert(!attributeData.IsDefault); @@ -1026,10 +1051,11 @@ public override ImmutableArray GetAttributes() : attributeData; } - ImmutableArray loadAndFilterAttributes(out bool isExtensionMethod, out bool isReadOnly) + ImmutableArray loadAndFilterAttributes(out bool isExtensionMethod, out bool isReadOnly, out bool hasRequiresUnsafeAttribute) { isExtensionMethod = false; isReadOnly = false; + hasRequiresUnsafeAttribute = false; var containingModule = _containingType.ContainingPEModule; if (!containingModule.TryGetNonEmptyCustomAttributes(_handle, out var customAttributeHandles)) @@ -1067,6 +1093,11 @@ ImmutableArray loadAndFilterAttributes(out bool isExtension if (containingModule.AttributeMatchesFilter(handle, AttributeDescription.ExtensionMarkerAttribute)) continue; + if (containingModule.AttributeMatchesFilter(handle, AttributeDescription.RequiresUnsafeAttribute)) + { + hasRequiresUnsafeAttribute = true; + } + builder.Add(new PEAttributeData(containingModule, handle)); } @@ -1449,6 +1480,20 @@ internal override bool IsDeclaredReadOnly } } + private bool RequiresUnsafe + { + get + { + if (!_packedFlags.RequiresUnsafePopulated) + { + bool hasRequiresUnsafeAttribute = _containingType.ContainingPEModule.Module.HasAttribute(_handle, AttributeDescription.RequiresUnsafeAttribute); + _packedFlags.InitializeRequiresUnsafe(ComputeRequiresUnsafe(hasRequiresUnsafeAttribute)); + } + + return _packedFlags.RequiresUnsafe; + } + } + internal override bool IsInitOnly { get @@ -1490,7 +1535,19 @@ internal override UseSiteInfo GetUseSiteInfo() if (diagnosticInfo == null && _containingType.ContainingPEModule.RefSafetyRulesVersion == PEModuleSymbol.RefSafetyRulesAttributeVersion.UnrecognizedAttribute) { - diagnosticInfo = new CSDiagnosticInfo(ErrorCode.ERR_UnrecognizedRefSafetyRulesAttributeVersion, this); + diagnosticInfo = new CSDiagnosticInfo(ErrorCode.ERR_UnrecognizedAttributeVersion, + this, + WellKnownTypes.GetMetadataName(WellKnownType.System_Runtime_CompilerServices_RefSafetyRulesAttribute), + "11"); + } + + if (diagnosticInfo == null && + _containingType.ContainingPEModule.MemorySafetyRulesVersion == PEModuleSymbol.MemorySafetyRulesAttributeVersion.UnrecognizedAttribute) + { + diagnosticInfo = new CSDiagnosticInfo(ErrorCode.ERR_UnrecognizedAttributeVersion, + this, + WellKnownTypes.GetMetadataName(WellKnownType.System_Runtime_CompilerServices_MemorySafetyRulesAttribute), + CSharpCompilationOptions.UpdatedMemorySafetyRulesVersion.ToString(CultureInfo.InvariantCulture)); } if (diagnosticInfo == null && GetUnmanagedCallersOnlyAttributeData(forceComplete: true) is UnmanagedCallersOnlyAttributeData data) @@ -1763,6 +1820,34 @@ internal sealed override bool HasUnscopedRefAttribute internal sealed override bool UseUpdatedEscapeRules => ContainingModule.UseUpdatedEscapeRules; + private bool ComputeRequiresUnsafe(bool hasRequiresUnsafeAttribute) + { + if (ContainingModule.UseUpdatedMemorySafetyRules) + { + Debug.Assert(AssociatedSymbol?.CallerUnsafeMode != CallerUnsafeMode.Implicit); + + return hasRequiresUnsafeAttribute || AssociatedSymbol?.CallerUnsafeMode == CallerUnsafeMode.Explicit; + } + + // This might be expensive, so we cache it in _packedFlags. + return this.HasParameterContainingPointerType() || ReturnType.ContainsPointerOrFunctionPointer(); + } + + internal sealed override CallerUnsafeMode CallerUnsafeMode + { + get + { + if (!RequiresUnsafe) + { + return CallerUnsafeMode.None; + } + + return ContainingModule.UseUpdatedMemorySafetyRules + ? CallerUnsafeMode.Explicit + : CallerUnsafeMode.Implicit; + } + } + internal override bool HasAsyncMethodBuilderAttribute(out TypeSymbol builderArgument) { builderArgument = _containingType.ContainingPEModule.TryDecodeAttributeWithTypeArgument(this.Handle, AttributeDescription.AsyncMethodBuilderAttribute); diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEModuleSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEModuleSymbol.cs index d0b790db180a..08876b5f5452 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEModuleSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEModuleSymbol.cs @@ -116,6 +116,16 @@ internal enum RefSafetyRulesAttributeVersion private RefSafetyRulesAttributeVersion _lazyRefSafetyRulesAttributeVersion; + internal enum MemorySafetyRulesAttributeVersion + { + Uninitialized = 0, + NoAttribute, + Updated, // https://github.com/dotnet/roslyn/issues/82546: rename to Version15 (or whatever the value ends up being) + UnrecognizedAttribute, + } + + private MemorySafetyRulesAttributeVersion _lazyMemorySafetyRulesAttributeVersion; + #nullable enable private DiagnosticInfo? _lazyCachedCompilerFeatureRequiredDiagnosticInfo = CSDiagnosticInfo.EmptyErrorInfo; @@ -747,6 +757,35 @@ RefSafetyRulesAttributeVersion getAttributeVersion() } } + internal override bool UseUpdatedMemorySafetyRules + => MemorySafetyRulesVersion == MemorySafetyRulesAttributeVersion.Updated; + + internal MemorySafetyRulesAttributeVersion MemorySafetyRulesVersion + { + get + { + if (_lazyMemorySafetyRulesAttributeVersion == MemorySafetyRulesAttributeVersion.Uninitialized) + { + _lazyMemorySafetyRulesAttributeVersion = getAttributeVersion(); + } + return _lazyMemorySafetyRulesAttributeVersion; + + MemorySafetyRulesAttributeVersion getAttributeVersion() + { + if (_module.HasMemorySafetyRulesAttribute(Token, out int version, out bool foundAttributeType)) + { + return version == CSharpCompilationOptions.UpdatedMemorySafetyRulesVersion + ? MemorySafetyRulesAttributeVersion.Updated + : MemorySafetyRulesAttributeVersion.UnrecognizedAttribute; + } + + return foundAttributeType + ? MemorySafetyRulesAttributeVersion.UnrecognizedAttribute + : MemorySafetyRulesAttributeVersion.NoAttribute; + } + } + } + internal sealed override ObsoleteAttributeData? ObsoleteAttributeData { get diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs index 233dbbedb28f..ebabba437201 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PENamedTypeSymbol.cs @@ -140,7 +140,7 @@ private sealed class UncommonProperties internal ImmutableArray lazyConditionalAttributeSymbols; internal ObsoleteAttributeData lazyObsoleteAttributeData = ObsoleteAttributeData.Uninitialized; internal AttributeUsageInfo lazyAttributeUsageInfo = AttributeUsageInfo.Null; - internal ThreeState lazyContainsExtensionMethods; + internal ThreeState lazyContainsExtensions; internal ThreeState lazyIsByRefLike; internal ThreeState lazyIsReadOnly; internal string lazyDefaultMemberName; @@ -150,6 +150,7 @@ private sealed class UncommonProperties internal ThreeState lazyHasCompilerLoweringPreserveAttribute = ThreeState.Unknown; internal ThreeState lazyHasInterpolatedStringHandlerAttribute = ThreeState.Unknown; internal ThreeState lazyHasRequiredMembers = ThreeState.Unknown; + internal ThreeState lazyHasUnionAttribute = ThreeState.Unknown; internal ThreeState lazyIsClosed = ThreeState.Unknown; internal ImmutableArray lazyFilePathChecksum = default; @@ -165,12 +166,14 @@ internal bool IsDefaultValue() lazyConditionalAttributeSymbols.IsDefault && lazyObsoleteAttributeData == ObsoleteAttributeData.Uninitialized && lazyAttributeUsageInfo.IsNull && - !lazyContainsExtensionMethods.HasValue() && + !lazyContainsExtensions.HasValue() && lazyDefaultMemberName == null && (object)lazyComImportCoClassType == (object)ErrorTypeSymbol.UnknownResultType && !lazyHasEmbeddedAttribute.HasValue() && + !lazyHasCompilerLoweringPreserveAttribute.HasValue() && !lazyHasInterpolatedStringHandlerAttribute.HasValue() && !lazyHasRequiredMembers.HasValue() && + !lazyHasUnionAttribute.HasValue() && !lazyIsClosed.HasValue() && (object)lazyCollectionBuilderAttributeData == CollectionBuilderAttributeData.Uninitialized && lazyFilePathChecksum.IsDefault && @@ -694,6 +697,25 @@ internal override bool HasCompilerLoweringPreserveAttribute } } + internal override bool IsUnionTypeCore + { + get + { + var uncommon = GetUncommonProperties(); + if (uncommon == s_noUncommonProperties) + { + return false; + } + + if (!uncommon.lazyHasUnionAttribute.HasValue()) + { + uncommon.lazyHasUnionAttribute = ContainingPEModule.Module.FindTargetAttribute(_handle, AttributeDescription.UnionAttribute).HasValue.ToThreeState(); + } + + return uncommon.lazyHasUnionAttribute.Value(); + } + } + internal override NamedTypeSymbol BaseTypeNoUseSiteDiagnostics { get @@ -969,7 +991,7 @@ ImmutableArray loadAndFilterAttributes(out bool hasRequired return []; } - var filterExtensionAttribute = MightContainExtensionMethods; + var filterExtensionAttribute = MightContainExtensions; var filterObsoleteAttribute = IsRefLikeType && ObsoleteAttributeData is null; var filterIsReadOnlyAttribute = IsReadOnly; var filterIsByRefLikeAttribute = IsRefLikeType; @@ -2031,7 +2053,7 @@ public sealed override bool AreLocalsZeroed get { throw ExceptionUtilities.Unreachable(); } } - public override bool MightContainExtensionMethods + public override bool MightContainExtensions { get { @@ -2041,7 +2063,7 @@ public override bool MightContainExtensionMethods return false; } - if (!uncommon.lazyContainsExtensionMethods.HasValue()) + if (!uncommon.lazyContainsExtensions.HasValue()) { var contains = ThreeState.False; // Dev11 supports extension methods defined on non-static @@ -2059,7 +2081,7 @@ public override bool MightContainExtensionMethods if ((object)containingAssembly != null) { contains = (moduleHasExtension - && containingAssembly.MightContainExtensionMethods).ToThreeState(); + && containingAssembly.MightContainExtensions).ToThreeState(); } else { @@ -2068,10 +2090,10 @@ public override bool MightContainExtensionMethods break; } - uncommon.lazyContainsExtensionMethods = contains; + uncommon.lazyContainsExtensions = contains; } - return uncommon.lazyContainsExtensionMethods.Value(); + return uncommon.lazyContainsExtensions.Value(); } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEPropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEPropertySymbol.cs index 3c9402624614..bbbf7694b5fc 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEPropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Metadata/PE/PEPropertySymbol.cs @@ -52,7 +52,7 @@ internal class PEPropertySymbol private struct PackedFlags { // Layout: - // |....................c|o|d|uu|rr|c|n|s| + // |.................|e|f|p|c|o|d|uu|rr|c|n|s| // // s = special name flag. 1 bit // n = runtime special name flag. 1 bit @@ -63,6 +63,8 @@ private struct PackedFlags // o = Obsolete flag. 1 bit // c = Custom attributes flag. 1 bit // p = Overload resolution priority populated. 1 bit + // f = Requires unsafe. 1 bit + // e = Requires unsafe populated. 1 bit private const int IsSpecialNameFlag = 1 << 0; private const int IsRuntimeSpecialNameFlag = 1 << 1; private const int CallMethodsDirectlyFlag = 1 << 2; @@ -74,6 +76,8 @@ private struct PackedFlags private const int IsObsoleteAttributePopulatedBit = 1 << 9; private const int IsCustomAttributesPopulatedBit = 1 << 10; private const int IsOverloadResolutionPriorityPopulatedBit = 1 << 11; + private const int RequiresUnsafeBit = 1 << 12; + private const int RequiresUnsafePopulatedBit = 1 << 13; private int _bits; @@ -120,6 +124,24 @@ public readonly bool TryGetHasUnscopedRefAttribute(out bool hasUnscopedRefAttrib return false; } + public void SetRequiresUnsafe(bool requiresUnsafe) + { + var bitsToSet = (requiresUnsafe ? RequiresUnsafeBit : 0) | RequiresUnsafePopulatedBit; + ThreadSafeFlagOperations.Set(ref _bits, bitsToSet); + } + + public readonly bool TryGetRequiresUnsafe(out bool requiresUnsafe) + { + if ((_bits & RequiresUnsafePopulatedBit) != 0) + { + requiresUnsafe = (_bits & RequiresUnsafeBit) != 0; + return true; + } + + requiresUnsafe = false; + return false; + } + public readonly bool IsSpecialName => (_bits & IsSpecialNameFlag) != 0; public readonly bool IsRuntimeSpecialName => (_bits & IsRuntimeSpecialNameFlag) != 0; public readonly bool CallMethodsDirectly => (_bits & CallMethodsDirectlyFlag) != 0; @@ -638,6 +660,45 @@ internal sealed override bool HasUnscopedRefAttribute } } + private bool RequiresUnsafe + { + get + { + if (!_flags.TryGetRequiresUnsafe(out bool requiresUnsafe)) + { + var containingPEModuleSymbol = (PEModuleSymbol)this.ContainingModule; + bool hasRequiresUnsafeAttribute = containingPEModuleSymbol.Module.HasAttribute(_handle, AttributeDescription.RequiresUnsafeAttribute); + requiresUnsafe = ComputeRequiresUnsafe(hasRequiresUnsafeAttribute); + _flags.SetRequiresUnsafe(requiresUnsafe); + } + + return requiresUnsafe; + } + } + + private bool ComputeRequiresUnsafe(bool hasRequiresUnsafeAttribute) + { + return ContainingModule.UseUpdatedMemorySafetyRules + ? hasRequiresUnsafeAttribute + // This might be expensive, so we cache it in _packedFlags. + : this.HasParameterContainingPointerType() || Type.ContainsPointerOrFunctionPointer(); + } + + internal sealed override CallerUnsafeMode CallerUnsafeMode + { + get + { + if (!RequiresUnsafe) + { + return CallerUnsafeMode.None; + } + + return ContainingModule.UseUpdatedMemorySafetyRules + ? CallerUnsafeMode.Explicit + : CallerUnsafeMode.Implicit; + } + } + public override ImmutableArray Parameters { get { return _parameters; } @@ -732,13 +793,14 @@ public override ImmutableArray GetAttributes() { if (!_flags.IsCustomAttributesPopulated) { - var attributes = loadAndFilterAttributes(out var hasRequiredMemberAttribute); + var attributes = loadAndFilterAttributes(out var hasRequiredMemberAttribute, out var hasRequiresUnsafeAttribute); if (!attributes.IsEmpty) { ImmutableInterlocked.InterlockedInitialize(ref AccessUncommonFields()._lazyCustomAttributes, attributes); } _flags.SetHasRequiredMemberAttribute(hasRequiredMemberAttribute); + _flags.SetRequiresUnsafe(ComputeRequiresUnsafe(hasRequiresUnsafeAttribute)); _flags.SetCustomAttributesPopulated(); } @@ -759,9 +821,10 @@ public override ImmutableArray GetAttributes() return result; } - ImmutableArray loadAndFilterAttributes(out bool hasRequiredMemberAttribute) + ImmutableArray loadAndFilterAttributes(out bool hasRequiredMemberAttribute, out bool hasRequiresUnsafeAttribute) { hasRequiredMemberAttribute = false; + hasRequiresUnsafeAttribute = false; var containingModule = (PEModuleSymbol)this.ContainingModule; if (!containingModule.TryGetNonEmptyCustomAttributes(_handle, out var customAttributeHandles)) @@ -787,6 +850,11 @@ ImmutableArray loadAndFilterAttributes(out bool hasRequired if (filterExtensionMarkerAttribute && containingModule.AttributeMatchesFilter(handle, AttributeDescription.ExtensionMarkerAttribute)) continue; + if (containingModule.AttributeMatchesFilter(handle, AttributeDescription.RequiresUnsafeAttribute)) + { + hasRequiresUnsafeAttribute = true; + } + builder.Add(new PEAttributeData(containingModule, handle)); } diff --git a/src/Compilers/CSharp/Portable/Symbols/MetadataOrSourceOrRetargetingAssemblySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/MetadataOrSourceOrRetargetingAssemblySymbol.cs index 903d295c75c9..2406298ba2b4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MetadataOrSourceOrRetargetingAssemblySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MetadataOrSourceOrRetargetingAssemblySymbol.cs @@ -17,6 +17,11 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols internal abstract class MetadataOrSourceOrRetargetingAssemblySymbol : NonMissingAssemblySymbol { + /// + /// Separate pool for collections commonly exceeding ArrayBuilder's size threshold + /// + private static readonly ObjectPool> s_assemblySymbolBuilderPool = new ObjectPool>(() => new ArrayBuilder()); + /// /// Determine whether this assembly has been granted access to . /// Assumes that the public key has been determined. The result will be cached. @@ -73,7 +78,8 @@ internal IVTConclusion MakeFinalIVTDetermination(AssemblySymbol potentialGiverOf protected bool IsDirectlyOrIndirectlyReferenced(AssemblySymbol potentialGiverOfAccess) { var checkedAssemblies = PooledHashSet.GetInstance(); - var queue = ArrayBuilder.GetInstance( + ArrayBuilder queue = s_assemblySymbolBuilderPool.Allocate(); + queue.EnsureCapacity( this.Modules[0].ReferencedAssemblySymbols.Length + (this is SourceAssemblySymbol { DeclaringCompilation.PreviousSubmission: { } } ? 1 : 0)); @@ -86,7 +92,12 @@ protected bool IsDirectlyOrIndirectlyReferenced(AssemblySymbol potentialGiverOfA } checkedAssemblies.Free(); - queue.Free(); + + // Do not call quue.Free, as the ArrayBuilder isn't associated with its standard pool and even if it were, we don't + // want the default freeing behavior of limiting pooled array size to ArrayBuilder.PooledArrayLengthLimitExclusive. + // Instead, we need to explicitly add this item back to our pool. + queue.Clear(); + s_assemblySymbolBuilderPool.Free(queue); return found; static bool checkReferences(AssemblySymbol current, AssemblySymbol potentialGiverOfAccess, PooledHashSet checkedAssemblies, ArrayBuilder queue) diff --git a/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs index cf5cee7c61ac..c0c07d551ecd 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MethodSymbol.cs @@ -98,6 +98,13 @@ public virtual bool IsGenericMethod internal abstract bool HasSpecialNameAttribute { get; } + /// + /// Returns the method-level runtime async setting from + /// RuntimeAsyncMethodGenerationAttribute, or + /// if no setting was specified. + /// + internal abstract ThreeState RuntimeAsyncMethodGenerationAttributeSetting { get; } + /// /// If a method is annotated with `[MemberNotNull(...)]` attributes, returns the list of members /// listed in those attributes. diff --git a/src/Compilers/CSharp/Portable/Symbols/MissingAssemblySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/MissingAssemblySymbol.cs index 1c1c488965ec..0652b1fb0fa5 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MissingAssemblySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MissingAssemblySymbol.cs @@ -200,7 +200,7 @@ internal override IEnumerable GetInternalsVisibleToAssemblyNames() return SpecializedCollections.EmptyEnumerable(); } - public override bool MightContainExtensionMethods + public override bool MightContainExtensions { get { diff --git a/src/Compilers/CSharp/Portable/Symbols/MissingModuleSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/MissingModuleSymbol.cs index e5ded01c4a24..f95d49a3b8aa 100644 --- a/src/Compilers/CSharp/Portable/Symbols/MissingModuleSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/MissingModuleSymbol.cs @@ -200,6 +200,8 @@ public sealed override bool AreLocalsZeroed internal sealed override bool UseUpdatedEscapeRules => false; + internal sealed override bool UseUpdatedMemorySafetyRules => false; + #nullable enable internal sealed override ObsoleteAttributeData? ObsoleteAttributeData => null; #nullable disable diff --git a/src/Compilers/CSharp/Portable/Symbols/ModuleSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ModuleSymbol.cs index b8d9cb2016cd..10aed148cc58 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ModuleSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ModuleSymbol.cs @@ -193,6 +193,8 @@ public override ImmutableArray DeclaringSyntaxReferences } } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + /// /// Returns an array of assembly identities for assemblies referenced by this module. /// Items at the same position from ReferencedAssemblies and from ReferencedAssemblySymbols @@ -319,6 +321,11 @@ internal AssemblySymbol GetReferencedAssemblySymbol(int referencedAssemblyIndex) internal abstract bool UseUpdatedEscapeRules { get; } + /// + /// + /// + internal abstract bool UseUpdatedMemorySafetyRules { get; } + /// /// Default char set for contained types, or null if not specified. /// diff --git a/src/Compilers/CSharp/Portable/Symbols/NamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/NamedTypeSymbol.cs index 2c1eb65ac358..caf0fcc7a458 100644 --- a/src/Compilers/CSharp/Portable/Symbols/NamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/NamedTypeSymbol.cs @@ -339,25 +339,17 @@ public ImmutableArray Indexers } /// - /// Returns true if this type might contain extension methods. If this property - /// returns false, there are no extension methods in this type. + /// Returns true if this type might contain extension members or methods. If this property + /// returns false, there are no extension members or methods in this type. /// /// - /// This property allows the search for extension methods to be narrowed quickly. + /// This property allows the search for extension members or methods to be narrowed quickly. /// - public abstract bool MightContainExtensionMethods { get; } - - /// Does not perform a full viability check - internal void GetExtensionMethods(ArrayBuilder methods, string nameOpt, int arity, LookupOptions options) - { - if (this.MightContainExtensionMethods) - { - DoGetExtensionMethods(methods, nameOpt, arity, options); - } - } + public abstract bool MightContainExtensions { get; } +#nullable enable /// Does not perform a full viability check - internal void DoGetExtensionMethods(ArrayBuilder methods, string nameOpt, int arity, LookupOptions options) + private void DoGetExtensionMethods(ArrayBuilder methods, string? nameOpt, int arity, LookupOptions options, PooledHashSet? implementationsToShadow) { var members = nameOpt == null ? this.GetMembersUnordered() @@ -377,14 +369,16 @@ internal void DoGetExtensionMethods(ArrayBuilder methods, string n continue; } - Debug.Assert(method.MethodKind != MethodKind.ReducedExtension); - methods.Add(method); + if (implementationsToShadow is null || !implementationsToShadow.Remove(method.OriginalDefinition)) + { + Debug.Assert(method.MethodKind != MethodKind.ReducedExtension); + methods.Add(method); + } } } } } -#nullable enable private static bool IsValidExtensionReceiverParameter(ParameterSymbol thisParam) { Debug.Assert(thisParam is not null); @@ -410,7 +404,7 @@ private static bool IsValidExtensionReceiverParameter(ParameterSymbol thisParam) } /// Does not perform a full viability check - internal void GetExtensionMembers(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, ConsList fieldsBeingBound) + internal void GetAllExtensionMembers(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, ConsList fieldsBeingBound) { Debug.Assert((options & ~(LookupOptions.AllMethodsOnArityZero | LookupOptions.MustBeInstance | LookupOptions.MustNotBeInstance | LookupOptions.MustBeInvocableIfMember @@ -418,37 +412,66 @@ internal void GetExtensionMembers(ArrayBuilder members, string? name, st Debug.Assert(name is not null || alternativeName is null); - if (!this.IsClassType() || !IsStatic || IsGenericType || !MightContainExtensionMethods) return; + if (!MightContainExtensions) + return; + + PooledHashSet? implementationsToShadow = null; + + if (this.IsClassType() && IsStatic && !IsGenericType) + { + doGetExtensionMembers(members, name, alternativeName, arity, options, ref implementationsToShadow, fieldsBeingBound); + } - foreach (NamedTypeSymbol nestedType in GetTypeMembers(name: "")) + if (!options.HasFlag(LookupOptions.MustBeOperator)) { - if (nestedType is not { IsExtension: true, ExtensionParameter: { } extensionParameter } - || !IsValidExtensionReceiverParameter(extensionParameter)) + DoGetExtensionMethods(members, name, arity, options, implementationsToShadow); + if (alternativeName is not null) { - continue; + DoGetExtensionMethods(members, alternativeName, arity, options, implementationsToShadow); } + } - var candidates = name is null || alternativeName is not null - ? nestedType.GetMembersUnordered() - : nestedType.GetMembers(name); + implementationsToShadow?.Free(); + + return; - foreach (var candidate in candidates) + void doGetExtensionMembers(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, ref PooledHashSet? implementationsToShadow, ConsList fieldsBeingBound) + { + foreach (NamedTypeSymbol nestedType in GetTypeMembers(name: "")) { - if (!SourceMemberContainerTypeSymbol.IsAllowedExtensionMember(candidate)) + if (nestedType is not { IsExtension: true, ExtensionParameter: { } extensionParameter } + || !IsValidExtensionReceiverParameter(extensionParameter)) { - // Not supported yet continue; } - if (extensionMemberMatches(candidate, name, alternativeName, arity, options, fieldsBeingBound)) + var candidates = name is null || alternativeName is not null + ? nestedType.GetMembersUnordered() + : nestedType.GetMembers(name); + + foreach (var candidate in candidates) { - members.Add(candidate); + if (!SourceMemberContainerTypeSymbol.IsAllowedExtensionMember(candidate)) + { + // Not supported yet + continue; + } + + if (extensionMemberMatches(candidate, name, alternativeName, arity, options, fieldsBeingBound)) + { + members.Add(candidate); + + if (candidate is MethodSymbol { IsStatic: false } shadows && + shadows.OriginalDefinition.TryGetCorrespondingExtensionImplementationMethod() is { } toShadow) + { + implementationsToShadow ??= PooledHashSet.GetInstance(); + implementationsToShadow.Add(toShadow); + } + } } } } - return; - static bool extensionMemberMatches(Symbol member, string? name, string? alternativeName, int arity, LookupOptions options, ConsList fieldsBeingBound) { if ((options & LookupOptions.MustBeInstance) != 0 && member.IsStatic) @@ -1778,6 +1801,50 @@ internal virtual FieldSymbol FixedElementField /// True if this is an interface type. internal abstract bool IsInterface { get; } + internal bool IsUnionType + { + get + { + return TypeKind is TypeKind.Class or TypeKind.Struct && + IsUnionTypeCore; + } + } + + internal abstract bool IsUnionTypeCore { get; } + + internal ImmutableArray UnionCaseTypes + { + get + { + if (!IsUnionType) + { + return []; + } + + var builder = ArrayBuilder.GetInstance(); + + foreach (var ctor in this.InstanceConstructors) + { + if (IsSuitableUnionConstructor(ctor)) + { + var candidate = ctor.Parameters[0].Type.StrippedType(); + if (!builder.Any(static (t1, t2) => t1.Equals(t2, TypeCompareKind.AllIgnoreOptions), candidate)) + { + builder.Add(candidate); + } + } + } + + return builder.ToImmutableAndFree(); + } + } + + internal static bool IsSuitableUnionConstructor(MethodSymbol ctor) + { + Debug.Assert(ctor.MethodKind is MethodKind.Constructor); + return ctor is { DeclaredAccessibility: Accessibility.Public, ParameterCount: 1, Parameters: [{ RefKind: RefKind.In or RefKind.None }] }; + } + /// /// Verify if the given type can be used to back a tuple type /// and return cardinality of that tuple type in . diff --git a/src/Compilers/CSharp/Portable/Symbols/NamespaceSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/NamespaceSymbol.cs index 53260471a974..a9f167878291 100644 --- a/src/Compilers/CSharp/Portable/Symbols/NamespaceSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/NamespaceSymbol.cs @@ -21,7 +21,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols internal abstract partial class NamespaceSymbol : NamespaceOrTypeSymbol, INamespaceSymbolInternal { // PERF: initialization of the following fields will allocate, so we make them lazy - private ImmutableArray _lazyTypesMightContainExtensionMethods; + private ImmutableArray _lazyTypesMightContainExtensions; private string _lazyQualifiedName; // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! @@ -218,6 +218,8 @@ internal sealed override ObsoleteAttributeData ObsoleteAttributeData get { return null; } } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + /// /// Returns an implicit type symbol for this namespace or null if there is none. This type /// wraps misplaced global code. @@ -312,58 +314,45 @@ internal NamespaceSymbol GetNestedNamespace(NameSyntax name) return null; } - private ImmutableArray TypesMightContainExtensionMethods + private ImmutableArray TypesMightContainExtensions { get { - var typesWithExtensionMethods = this._lazyTypesMightContainExtensionMethods; + var typesWithExtensionMethods = this._lazyTypesMightContainExtensions; if (typesWithExtensionMethods.IsDefault) { - this._lazyTypesMightContainExtensionMethods = this.GetTypeMembersUnordered().WhereAsArray(t => t.MightContainExtensionMethods); - typesWithExtensionMethods = this._lazyTypesMightContainExtensionMethods; + this._lazyTypesMightContainExtensions = this.GetTypeMembersUnordered().WhereAsArray(t => t.MightContainExtensions); + typesWithExtensionMethods = this._lazyTypesMightContainExtensions; } return typesWithExtensionMethods; } } +#nullable enable /// - /// Add all extension methods in this namespace to the given list. If name or arity + /// Add all extension members and methods in this namespace to the given list. If name or arity /// or both are provided, only those extension methods that match are included. /// - /// Methods list - /// Optional method name - /// Method arity - /// Lookup options /// Does not perform a full viability check - internal virtual void GetExtensionMethods(ArrayBuilder methods, string nameOpt, int arity, LookupOptions options) + internal virtual void GetAllExtensionMembers(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, ConsList fieldsBeingBound) { var assembly = this.ContainingAssembly; - // Only MergedAssemblySymbol should have a null ContainingAssembly - // and MergedAssemblySymbol overrides GetExtensionMethods. + // Only MergedNamespaceSymbol should have a null ContainingAssembly + // and MergedNamespaceSymbol overrides GetAllExtensionMembers. Debug.Assert((object)assembly != null); - if (!assembly.MightContainExtensionMethods) + if (!assembly.MightContainExtensions) { return; } - var typesWithExtensionMethods = this.TypesMightContainExtensionMethods; + var typesWithExtensionMethods = this.TypesMightContainExtensions; foreach (var type in typesWithExtensionMethods) { - type.DoGetExtensionMethods(methods, nameOpt, arity, options); - } - } - -#nullable enable - /// Does not perform a full viability check - internal virtual void GetExtensionMembers(ArrayBuilder members, string? name, string? alternativeName, int arity, LookupOptions options, ConsList fieldsBeingBound) - { - foreach (var type in this.GetTypeMembersUnordered()) - { - type.GetExtensionMembers(members, name, alternativeName, arity, options, fieldsBeingBound); + type.GetAllExtensionMembers(members, name, alternativeName, arity, options, fieldsBeingBound); } } #nullable disable diff --git a/src/Compilers/CSharp/Portable/Symbols/NativeIntegerTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/NativeIntegerTypeSymbol.cs index c4fcddc5aef7..527127790ced 100644 --- a/src/Compilers/CSharp/Portable/Symbols/NativeIntegerTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/NativeIntegerTypeSymbol.cs @@ -213,6 +213,8 @@ internal override bool Equals(TypeSymbol? other, TypeCompareKind comparison) internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + #if !DEBUG void Cci.IReference.Dispatch(Cci.MetadataVisitor visitor) { @@ -362,6 +364,10 @@ internal NativeIntegerMethodSymbol(NativeIntegerTypeSymbol container, MethodSymb public override ImmutableArray TypeParameters => ImmutableArray.Empty; + public override bool IsAsync => UnderlyingMethod.IsAsync; + + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); + public override ImmutableArray Parameters { get diff --git a/src/Compilers/CSharp/Portable/Symbols/ParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ParameterSymbol.cs index 66964d085548..4ac41a92b5df 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ParameterSymbol.cs @@ -450,6 +450,8 @@ internal sealed override ObsoleteAttributeData? ObsoleteAttributeData internal abstract bool UseUpdatedEscapeRules { get; } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + protected sealed override bool IsHighestPriorityUseSiteErrorCode(int code) => code is (int)ErrorCode.ERR_UnsupportedCompilerFeature or (int)ErrorCode.ERR_BogusType; public override bool HasUnsupportedMetadata diff --git a/src/Compilers/CSharp/Portable/Symbols/PropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/PropertySymbol.cs index 386df35f6125..0b57c2e131ea 100644 --- a/src/Compilers/CSharp/Portable/Symbols/PropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/PropertySymbol.cs @@ -166,8 +166,7 @@ public bool IsReadOnly { get { - var property = (PropertySymbol)this.GetLeastOverriddenMember(this.ContainingType); - return (object)property.SetMethod == null; + return this.GetOwnOrInheritedSetMethod() is null; } } @@ -178,8 +177,7 @@ public bool IsWriteOnly { get { - var property = (PropertySymbol)this.GetLeastOverriddenMember(this.ContainingType); - return (object)property.GetMethod == null; + return this.GetOwnOrInheritedGetMethod() is null; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/PublicModel/AssemblySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/PublicModel/AssemblySymbol.cs index fe3bd87262fb..0a5a9d963a39 100644 --- a/src/Compilers/CSharp/Portable/Symbols/PublicModel/AssemblySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/PublicModel/AssemblySymbol.cs @@ -45,7 +45,7 @@ IEnumerable IAssemblySymbol.Modules ICollection IAssemblySymbol.NamespaceNames => UnderlyingAssemblySymbol.NamespaceNames; - bool IAssemblySymbol.MightContainExtensionMethods => UnderlyingAssemblySymbol.MightContainExtensionMethods; + bool IAssemblySymbol.MightContainExtensionMethods => UnderlyingAssemblySymbol.MightContainExtensions; AssemblyMetadata IAssemblySymbol.GetMetadata() => UnderlyingAssemblySymbol.GetMetadata(); diff --git a/src/Compilers/CSharp/Portable/Symbols/PublicModel/NamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/PublicModel/NamedTypeSymbol.cs index 6d0a697c23b1..6b99400ad8a0 100644 --- a/src/Compilers/CSharp/Portable/Symbols/PublicModel/NamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/PublicModel/NamedTypeSymbol.cs @@ -190,7 +190,7 @@ INamedTypeSymbol INamedTypeSymbol.TupleUnderlyingType bool INamedTypeSymbol.IsImplicitClass => UnderlyingNamedTypeSymbol.IsImplicitClass; - bool INamedTypeSymbol.MightContainExtensionMethods => UnderlyingNamedTypeSymbol.MightContainExtensionMethods; + bool INamedTypeSymbol.MightContainExtensionMethods => UnderlyingNamedTypeSymbol.MightContainExtensions; bool INamedTypeSymbol.IsSerializable => UnderlyingNamedTypeSymbol.IsSerializable; diff --git a/src/Compilers/CSharp/Portable/Symbols/RangeVariableSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/RangeVariableSymbol.cs index 725d4c6a9bf8..1bd5a322b9dd 100644 --- a/src/Compilers/CSharp/Portable/Symbols/RangeVariableSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/RangeVariableSymbol.cs @@ -13,7 +13,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Symbols /// A RangeVariableSymbol represents an identifier introduced in a query expression as the /// identifier of a "from" clause, an "into" query continuation, a "let" clause, or a "join" clause. /// - internal class RangeVariableSymbol : Symbol + internal sealed class RangeVariableSymbol : Symbol { private readonly string _name; private readonly Location? _location; @@ -124,6 +124,8 @@ internal sealed override ObsoleteAttributeData? ObsoleteAttributeData get { return null; } } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + public override Accessibility DeclaredAccessibility { get diff --git a/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs index 11902fc4ea73..323d2d0f4309 100644 --- a/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/ReducedExtensionMethodSymbol.cs @@ -503,6 +503,8 @@ public override TypeWithAnnotations ReturnTypeWithAnnotations public override FlowAnalysisAnnotations FlowAnalysisAnnotations => _reducedFrom.FlowAnalysisAnnotations; + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); + public override ImmutableArray RefCustomModifiers { get { return _typeMap.SubstituteCustomModifiers(_reducedFrom.RefCustomModifiers); } @@ -602,6 +604,8 @@ public override int GetHashCode() internal sealed override bool HasUnscopedRefAttribute => false; + internal sealed override CallerUnsafeMode CallerUnsafeMode => throw ExceptionUtilities.Unreachable(); + internal sealed override bool UseUpdatedEscapeRules => _reducedFrom.UseUpdatedEscapeRules; internal sealed override bool HasAsyncMethodBuilderAttribute(out TypeSymbol builderArgument) diff --git a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingAssemblySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingAssemblySymbol.cs index 0306d4244b74..f043b22aa151 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingAssemblySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingAssemblySymbol.cs @@ -284,11 +284,11 @@ public override ICollection NamespaceNames } } - public override bool MightContainExtensionMethods + public override bool MightContainExtensions { get { - return _underlyingAssembly.MightContainExtensionMethods; + return _underlyingAssembly.MightContainExtensions; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingMethodSymbol.cs index 2c8d3bca4077..2a68f3ffbf9b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingMethodSymbol.cs @@ -126,6 +126,10 @@ public override ImmutableArray TypeArgumentsWithAnnotations } } + public override bool IsAsync => _underlyingMethod.IsAsync; + + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); + public override TypeWithAnnotations ReturnTypeWithAnnotations { get diff --git a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingModuleSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingModuleSymbol.cs index 5c51325b0807..34301c00be71 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingModuleSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingModuleSymbol.cs @@ -319,6 +319,8 @@ public sealed override bool AreLocalsZeroed internal override bool UseUpdatedEscapeRules => _underlyingModule.UseUpdatedEscapeRules; + internal override bool UseUpdatedMemorySafetyRules => _underlyingModule.UseUpdatedMemorySafetyRules; + #nullable enable internal sealed override ObsoleteAttributeData? ObsoleteAttributeData => _underlyingModule.ObsoleteAttributeData; diff --git a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingNamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingNamedTypeSymbol.cs index da738bdbe44c..3888c230e1a7 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingNamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingNamedTypeSymbol.cs @@ -480,6 +480,8 @@ internal sealed override bool HasAsyncMethodBuilderAttribute(out TypeSymbol? bui internal override bool HasCompilerLoweringPreserveAttribute => _underlyingType.HasCompilerLoweringPreserveAttribute; + internal override bool IsUnionTypeCore => _underlyingType.IsUnionTypeCore; + internal override string? ExtensionGroupingName => _underlyingType.ExtensionGroupingName; diff --git a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingNamespaceSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingNamespaceSymbol.cs index 88c62597281d..4c9ec82a7940 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingNamespaceSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Retargeting/RetargetingNamespaceSymbol.cs @@ -240,20 +240,7 @@ public override string Name return this.RetargetingTranslator.Retarget(underlying, RetargetOptions.RetargetPrimitiveTypesByName); } -#nullable disable - - internal override void GetExtensionMethods(ArrayBuilder methods, string nameOpt, int arity, LookupOptions options) - { - var underlyingMethods = ArrayBuilder.GetInstance(); - _underlyingNamespace.GetExtensionMethods(underlyingMethods, nameOpt, arity, options); - foreach (var underlyingMethod in underlyingMethods) - { - methods.Add(this.RetargetingTranslator.Retarget(underlyingMethod)); - } - underlyingMethods.Free(); - } - - internal sealed override CSharpCompilation DeclaringCompilation // perf, not correctness + internal sealed override CSharpCompilation? DeclaringCompilation // perf, not correctness { get { return null; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs index dadb95533fb2..74ba90670dda 100644 --- a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyMethodSymbol.cs @@ -186,8 +186,12 @@ internal override bool IsMetadataFinal internal sealed override bool UseUpdatedEscapeRules => true; + internal sealed override CallerUnsafeMode CallerUnsafeMode => throw ExceptionUtilities.Unreachable(); + internal sealed override int TryGetOverloadResolutionPriority() => throw ExceptionUtilities.Unreachable(); + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); + #endregion } } diff --git a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyPropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyPropertySymbol.cs index 1ec96ab09bb7..72f9f5747089 100644 --- a/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyPropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/SignatureOnlyPropertySymbol.cs @@ -105,6 +105,8 @@ public SignatureOnlyPropertySymbol( internal override int TryGetOverloadResolutionPriority() => throw ExceptionUtilities.Unreachable(); + internal override CallerUnsafeMode CallerUnsafeMode => throw ExceptionUtilities.Unreachable(); + #endregion Not used by PropertySignatureComparer } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/AttributeLocation.cs b/src/Compilers/CSharp/Portable/Symbols/Source/AttributeLocation.cs index bd2c122509c1..f40a531082b9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/AttributeLocation.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/AttributeLocation.cs @@ -30,9 +30,10 @@ internal enum AttributeLocation : short Parameter = 1 << 7, Return = 1 << 8, TypeParameter = 1 << 9, + Extension = 1 << 10, // must be the last: - Unknown = 1 << 10, + Unknown = 1 << 11, } internal static class AttributeLocationExtensions @@ -91,6 +92,7 @@ internal static string ToDisplayString(this AttributeLocation locations) result.Append("typevar"); break; + case AttributeLocation.Extension: default: throw ExceptionUtilities.UnexpectedValue(i); } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ImplicitNamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ImplicitNamedTypeSymbol.cs index 71cf1826fe2b..6540e1cadb1b 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ImplicitNamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ImplicitNamedTypeSymbol.cs @@ -173,6 +173,8 @@ internal override ObsoleteAttributeData ObsoleteAttributeData internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal override bool IsInterpolatedStringHandlerType => false; internal sealed override ParameterSymbol ExtensionParameter => null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/LambdaSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/LambdaSymbol.cs index 5e591535fc54..df9ec5cdc0d1 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/LambdaSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/LambdaSymbol.cs @@ -23,6 +23,7 @@ internal sealed class LambdaSymbol : SourceMethodSymbol private RefKind _refKind; private ImmutableArray _refCustomModifiers; private TypeWithAnnotations _returnType; + private bool _runtimeAsyncEnabledChangedDuringInference; private readonly bool _isSynthesized; private readonly bool _isAsync; private readonly bool _isStatic; @@ -168,13 +169,22 @@ public override TypeWithAnnotations ReturnTypeWithAnnotations internal void SetInferredReturnType(RefKind refKind, TypeWithAnnotations inferredReturnType) { Debug.Assert(inferredReturnType.HasType); - Debug.Assert(_returnType.Type.IsErrorType()); + Debug.Assert((object)_returnType.Type == ReturnTypeIsBeingInferred); Debug.Assert(refKind != RefKind.RefReadOnly); + + var runtimeAsyncEnabledDuringInference = IsAsync && _binder.Compilation.IsRuntimeAsyncEnabledIn(this); _refKind = refKind; _refCustomModifiers = []; _returnType = inferredReturnType; + _runtimeAsyncEnabledChangedDuringInference = IsAsync && runtimeAsyncEnabledDuringInference != _binder.Compilation.IsRuntimeAsyncEnabledIn(this); } + /// + /// True if changed whether returns true for this method. + /// In such cases, the lambda body needs to be re-bound to ensure correct handling of await expressions. + /// + public bool RuntimeAsyncEnabledChangedDuringInference => _runtimeAsyncEnabledChangedDuringInference; + internal override bool IsExplicitInterfaceImplementation { get { return false; } @@ -416,6 +426,8 @@ internal override bool GenerateDebugInfo internal override bool IsInitOnly => false; + internal override bool IsUnsafe => false; + public override ImmutableArray> GetTypeParameterConstraintTypes() => ImmutableArray>.Empty; public override ImmutableArray GetTypeParameterConstraintKinds() => ImmutableArray.Empty; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs index 16d59204128d..73ac2d23560c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/LocalFunctionSymbol.cs @@ -126,6 +126,15 @@ internal void GetDeclarationDiagnostics(BindingDiagnosticBag addTo) GetReturnTypeAttributes(); var compilation = DeclaringCompilation; + var location = Syntax.Identifier.GetLocation(); + + if (NeedsSynthesizedRequiresUnsafeAttribute) + { + Debug.Assert(CallerUnsafeMode == CallerUnsafeMode.Explicit); + MessageID.IDS_FeatureUnsafeEvolution.CheckFeatureAvailability(addTo, compilation, location); + Binder.GetWellKnownTypeMember(compilation, WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor, addTo, location); + } + ParameterHelpers.EnsureRefKindAttributesExist(compilation, Parameters, addTo, modifyCompilation: false); ParameterHelpers.EnsureParamCollectionAttributeExists(compilation, Parameters, addTo, modifyCompilation: false); ParameterHelpers.EnsureNativeIntegerAttributeExists(compilation, Parameters, addTo, modifyCompilation: false); @@ -142,7 +151,7 @@ internal void GetDeclarationDiagnostics(BindingDiagnosticBag addTo) ContainingSymbol is SynthesizedSimpleProgramEntryPointSymbol && compilation.HasEntryPointSignature(this, diagnostics).IsCandidate) { - addTo.Add(ErrorCode.WRN_MainIgnored, Syntax.Identifier.GetLocation(), this); + addTo.Add(ErrorCode.WRN_MainIgnored, location, this); } addTo.AddRangeAndFree(diagnostics); @@ -391,7 +400,7 @@ protected override void NoteAttributesComplete(bool forReturnType) { } public override bool IsExtern => (_declarationModifiers & DeclarationModifiers.Extern) != 0; - public bool IsUnsafe => (_declarationModifiers & DeclarationModifiers.Unsafe) != 0; + internal override bool IsUnsafe => (_declarationModifiers & DeclarationModifiers.Unsafe) != 0; internal bool IsExpressionBodied => Syntax is { Body: null, ExpressionBody: object _ }; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/ParameterHelpers.cs b/src/Compilers/CSharp/Portable/Symbols/Source/ParameterHelpers.cs index 3d65bfb827c4..7dceb83374f2 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/ParameterHelpers.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/ParameterHelpers.cs @@ -997,6 +997,7 @@ internal static bool ReportDefaultParameterErrors( } else if (!conversion.Exists || conversion.IsUserDefined || + conversion.IsUnion || // https://github.com/dotnet/roslyn/issues/82636: Add coverage conversion.IsIdentity && parameterType.SpecialType == SpecialType.System_Object && defaultExpression.Type.IsDynamic()) { // If we had no implicit conversion, or a user-defined conversion, report an error. diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceAssemblySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceAssemblySymbol.cs index 6a3ee95df1c9..6d4553f484f7 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceAssemblySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceAssemblySymbol.cs @@ -86,7 +86,7 @@ internal sealed partial class SourceAssemblySymbol : MetadataOrSourceAssemblySym /// private ConcurrentSet _lazyOmittedAttributeIndices; - private ThreeState _lazyContainsExtensionMethods; + private ThreeState _lazyContainsExtensions; /// /// Map for storing effectively private or effectively internal fields declared in this assembly but never initialized nor assigned. @@ -1897,16 +1897,18 @@ internal bool DeclaresTheObjectClass } } - public override bool MightContainExtensionMethods + /// + /// This method returns true until is + /// called, after which the correct value will be returned. In other words, + /// the return value may change from true to false on subsequent calls. + /// + public override bool MightContainExtensions { get { - // Note this method returns true until all ContainsExtensionMethods is - // called, after which the correct value will be returned. In other words, - // the return value may change from true to false on subsequent calls. - if (_lazyContainsExtensionMethods.HasValue()) + if (_lazyContainsExtensions.HasValue()) { - return _lazyContainsExtensionMethods.Value(); + return _lazyContainsExtensions.Value(); } return true; } @@ -1942,9 +1944,9 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r CSharpCompilationOptions options = _compilation.Options; bool isBuildingNetModule = options.OutputKind.IsNetModule(); - bool containsExtensionMethods = this.ContainsExtensionMethods(); + bool containsExtensions = this.ContainsExtensions(); - if (containsExtensionMethods) + if (containsExtensions) { // No need to check if [Extension] attribute was explicitly set since // we'll issue CS1112 error in those cases and won't generate IL. @@ -2041,25 +2043,25 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r /// /// Returns true if and only if at least one type within the assembly contains - /// extension methods. Note, this method is expensive since it potentially + /// extension members or methods. Note, this method is expensive since it potentially /// inspects all types within the assembly. The expectation is that this method is /// only called at emit time, when all types have been or will be traversed anyway. /// - private bool ContainsExtensionMethods() + private bool ContainsExtensions() { - if (!_lazyContainsExtensionMethods.HasValue()) + if (!_lazyContainsExtensions.HasValue()) { - _lazyContainsExtensionMethods = ContainsExtensionMethods(_modules).ToThreeState(); + _lazyContainsExtensions = ContainsExtensions(_modules).ToThreeState(); } - return _lazyContainsExtensionMethods.Value(); + return _lazyContainsExtensions.Value(); } - private static bool ContainsExtensionMethods(ImmutableArray modules) + private static bool ContainsExtensions(ImmutableArray modules) { foreach (var module in modules) { - if (ContainsExtensionMethods(module.GlobalNamespace)) + if (ContainsExtensions(module.GlobalNamespace)) { return true; } @@ -2067,20 +2069,20 @@ private static bool ContainsExtensionMethods(ImmutableArray module return false; } - private static bool ContainsExtensionMethods(NamespaceSymbol ns) + private static bool ContainsExtensions(NamespaceSymbol ns) { foreach (var member in ns.GetMembersUnordered()) { switch (member.Kind) { case SymbolKind.Namespace: - if (ContainsExtensionMethods((NamespaceSymbol)member)) + if (ContainsExtensions((NamespaceSymbol)member)) { return true; } break; case SymbolKind.NamedType: - if (((NamedTypeSymbol)member).MightContainExtensionMethods) + if (((NamedTypeSymbol)member).MightContainExtensions) { return true; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceConstructorSymbolBase.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceConstructorSymbolBase.cs index 79df02470402..6f675ebc567f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceConstructorSymbolBase.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceConstructorSymbolBase.cs @@ -28,7 +28,7 @@ protected SourceConstructorSymbolBase( (DeclarationModifiers declarationModifiers, Flags flags) modifiersAndFlags) : base(containingType, syntax.GetReference(), location, isIterator, modifiersAndFlags) { - Debug.Assert(syntax.Kind() is SyntaxKind.ConstructorDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration or SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration); + Debug.Assert(syntax.Kind() is SyntaxKind.ConstructorDeclaration or SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration or SyntaxKind.ClassDeclaration or SyntaxKind.StructDeclaration or SyntaxKind.UnionDeclaration); } protected sealed override void MethodChecks(BindingDiagnosticBag diagnostics) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs index 613b1f43a46b..31326033d926 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceEventSymbol.cs @@ -377,6 +377,11 @@ protected sealed override void DecodeWellKnownAttributeImpl(ref DecodeWellKnownA { diagnostics.Add(ErrorCode.ERR_UnscopedRefAttributeUnsupportedMemberTarget, arguments.AttributeSyntaxOpt!.Location); } + else if (attribute.IsTargetAttribute(AttributeDescription.RequiresUnsafeAttribute)) + { + if (ContainingModule.UseUpdatedMemorySafetyRules) MessageID.IDS_FeatureUnsafeEvolution.CheckFeatureAvailability(diagnostics, arguments.AttributeSyntaxOpt!); + arguments.GetOrCreateData().HasRequiresUnsafeAttribute = true; + } } internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, ref ArrayBuilder? attributes) @@ -406,6 +411,12 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r { AddSynthesizedAttribute(ref attributes, moduleBuilder.SynthesizeNullableAttributeIfNecessary(this, containingType.GetNullableContextValue(), type)); } + + if (NeedsSynthesizedRequiresUnsafeAttribute) + { + Debug.Assert(CallerUnsafeMode == CallerUnsafeMode.Explicit); + AddSynthesizedAttribute(ref attributes, compilation.TrySynthesizeAttribute(WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor)); + } } internal sealed override bool IsDirectlyExcludedFromCodeCoverage => @@ -465,6 +476,34 @@ private bool IsUnsafe get { return (_modifiers & DeclarationModifiers.Unsafe) != 0; } } + internal bool HasRequiresUnsafeAttribute => GetDecodedWellKnownAttributeData()?.HasRequiresUnsafeAttribute == true; + + internal sealed override CallerUnsafeMode CallerUnsafeMode + { + get + { + if (ContainingModule.UseUpdatedMemorySafetyRules) + { + return HasRequiresUnsafeAttribute || IsExtern + ? CallerUnsafeMode.Explicit + : CallerUnsafeMode.None; + } + + return Type.ContainsPointerOrFunctionPointer() + ? CallerUnsafeMode.Implicit : CallerUnsafeMode.None; + } + } + + private bool NeedsSynthesizedRequiresUnsafeAttribute + { + get + { + return ContainingModule.UseUpdatedMemorySafetyRules && + !HasRequiresUnsafeAttribute && + IsExtern; + } + } + public sealed override Accessibility DeclaredAccessibility { get { return ModifierUtils.EffectiveAccessibility(_modifiers); } @@ -827,7 +866,8 @@ internal static string GetAccessorName(string eventName, bool isAdder) protected TypeWithAnnotations BindEventType(Binder binder, TypeSyntax typeSyntax, BindingDiagnosticBag diagnostics) { // NOTE: no point in reporting unsafe errors in the return type - anything unsafe will either - // fail to be a delegate or will be (invalidly) passed as a type argument. + // fail to be a delegate or will be (invalidly) passed as a type argument. + // Actually, this is wrong (e.g., `Action` is valid): https://github.com/dotnet/roslyn/issues/81944. // Prevent constraint checking. binder = binder.WithAdditionalFlagsAndContainingMemberOrLambda(BinderFlags.SuppressConstraintChecks | BinderFlags.SuppressUnsafeDiagnostics, this); @@ -853,6 +893,13 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions, compilation.EnsureNullableAttributeExists(diagnostics, location, modifyCompilation: true); } + if (NeedsSynthesizedRequiresUnsafeAttribute) + { + Debug.Assert(CallerUnsafeMode == CallerUnsafeMode.Explicit); + MessageID.IDS_FeatureUnsafeEvolution.CheckFeatureAvailability(diagnostics, compilation, location); + Binder.GetWellKnownTypeMember(compilation, WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor, diagnostics, location); + } + EventSymbol? explicitlyImplementedEvent = ExplicitInterfaceImplementations.FirstOrDefault(); if (explicitlyImplementedEvent is object) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs index 709595bb26d2..cb1e3c900c8f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol.cs @@ -214,7 +214,7 @@ public bool SetHasDeclaredRequiredMembers(bool value) private int _lazyKnownCircularStruct; private LexicalSortKey _lazyLexicalSortKey = LexicalSortKey.NotInitialized; - private ThreeState _lazyContainsExtensionMethods; + private ThreeState _lazyContainsExtensions; private ThreeState _lazyAnyMemberHasAttributes; // Tracked by https://github.com/dotnet/roslyn/issues/78827 : Optimize by moving some fields into "uncommon" class field? @@ -338,7 +338,7 @@ private DeclarationModifiers MakeModifiers(TypeKind typeKind, BindingDiagnosticB case TypeKind.Struct: allowedModifiers |= DeclarationModifiers.Partial | DeclarationModifiers.ReadOnly | DeclarationModifiers.Unsafe; - if (!this.IsRecordStruct) + if (!this.IsRecordStruct && !this.IsUnionDeclaration) { allowedModifiers |= DeclarationModifiers.Ref; } @@ -384,6 +384,14 @@ private DeclarationModifiers MakeModifiers(TypeKind typeKind, BindingDiagnosticB diagnostics.Add(ErrorCode.ERR_SealedStaticClass, GetFirstLocation(), this); } + if (!modifierErrors && + typeKind == TypeKind.Delegate && + (mods & DeclarationModifiers.Unsafe) == DeclarationModifiers.Unsafe && + this.ContainingModule.UseUpdatedMemorySafetyRules) + { + diagnostics.Add(ErrorCode.WRN_UnsafeMeaningless, GetFirstLocation()); + } + switch (typeKind) { case TypeKind.Interface: @@ -982,6 +990,14 @@ internal override bool IsRecordStruct } } + internal bool IsUnionDeclaration + { + get + { + return this.declaration.Declarations[0].Kind is DeclarationKind.Union; + } + } + public override bool IsImplicitlyDeclared { get @@ -1565,7 +1581,7 @@ internal override bool HasPossibleWellKnownCloneMethod() internal override ImmutableArray GetSimpleNonTypeMembers(string name) { - if (_lazyMembersDictionary != null || declaration.ContainsExtensionDeclarations || declaration.MemberNames.Contains(name) || declaration.Kind is DeclarationKind.Record or DeclarationKind.RecordStruct) + if (_lazyMembersDictionary != null || declaration.ContainsExtensionDeclarations || declaration.MemberNames.Contains(name) || declaration.Kind is DeclarationKind.Record or DeclarationKind.RecordStruct or DeclarationKind.Union) { return GetMembers(name); } @@ -1857,7 +1873,7 @@ internal override IEnumerable GetInstanceFieldsAndEvents() return result; } - protected void AfterMembersChecks(BindingDiagnosticBag diagnostics) + protected virtual void AfterMembersChecks(BindingDiagnosticBag diagnostics) { var compilation = DeclaringCompilation; var location = GetFirstLocation(); @@ -1979,6 +1995,26 @@ protected void AfterMembersChecks(BindingDiagnosticBag diagnostics) SourceOrdinaryMethodSymbol.CheckExtensionAttributeAvailability(DeclaringCompilation, location, diagnostics); } + if (IsUnionDeclaration) + { + var fields = this.GetFieldsToEmit(); + foreach (var field in fields) + { + if (!field.IsStatic && field.AssociatedSymbol is not SynthesizedUnionValuePropertySymbol) + { + diagnostics.Add(ErrorCode.ERR_InstanceFieldInUnion, field.GetFirstLocation()); + } + } + + foreach (var ctor in InstanceConstructors) + { + if (ctor.ParameterCount == 1 && ctor is not SynthesizedUnionCtor) + { + diagnostics.Add(ErrorCode.ERR_InstanceCtorWithOneParameterInUnion, ctor.GetFirstLocation()); + } + } + } + return; bool hasBaseTypeOrInterface(Func predicate) @@ -3284,7 +3320,7 @@ public DeclaredMembersAndInitializers( AssertInitializers(instanceInitializers, compilation); Debug.Assert(!nonTypeMembersWithPartialImplementations.Any(static s => s is TypeSymbol)); - Debug.Assert(declarationWithParameters is object == primaryConstructor is object); + Debug.Assert(declarationWithParameters is object || primaryConstructor is null); this.NonTypeMembersWithPartialImplementations = nonTypeMembersWithPartialImplementations; this.StaticInitializers = staticInitializers; @@ -3480,6 +3516,12 @@ public void UpdateIsNullableEnabledForConstructorsAndFields(bool useStatic, CSha isNullableEnabled = isNullableEnabled || compilation.IsNullableAnalysisEnabledIn(syntax); } + public void UpdateIsNullableEnabledForConstructorsAndFields(bool useStatic, bool value) + { + ref bool isNullableEnabled = ref GetIsNullableEnabledForConstructorsAndFields(useStatic); + isNullableEnabled = isNullableEnabled || value; + } + private ref bool GetIsNullableEnabledForConstructorsAndFields(bool useStatic) { return ref useStatic ? ref IsNullableEnabledForStaticConstructorsAndFields : ref IsNullableEnabledForInstanceConstructorsAndFields; @@ -3770,6 +3812,8 @@ internal IEnumerable GetMethodsPossiblyCapturingPrimar internal ImmutableArray GetMembersToMatchAgainstDeclarationSpan() { + Debug.Assert(HasPrimaryConstructor); + var declared = Volatile.Read(ref _lazyDeclaredMembersAndInitializers); if (declared is not null && declared != DeclaredMembersAndInitializers.UninitializedSentinel) { @@ -3786,6 +3830,8 @@ internal ImmutableArray GetMembersToMatchAgainstDeclarationSpan() internal ImmutableArray GetCandidateMembersForLookup(string name) { + Debug.Assert(HasPrimaryConstructor); + if (this is { IsRecord: true } or { IsRecordStruct: true } || this.state.HasComplete(CompletionPart.Members)) { @@ -3932,6 +3978,7 @@ private void AddDeclaredNontypeMembers(DeclaredMembersAndInitializersBuilder bui case SyntaxKind.ClassDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.RecordStructDeclaration: case SyntaxKind.ExtensionBlockDeclaration: @@ -3959,20 +4006,28 @@ void noteTypeParameters(TypeDeclarationSyntax syntax, DeclaredMembersAndInitiali if (builder.DeclarationWithParameters is null) { builder.DeclarationWithParameters = syntax; - var ctor = new SynthesizedPrimaryConstructor(this, syntax); - if (this.IsStatic) + if (syntax.Kind() is SyntaxKind.UnionDeclaration) { - diagnostics.Add(ErrorCode.ERR_ConstructorInStaticClass, syntax.Identifier.GetLocation()); + builder.UpdateIsNullableEnabledForConstructorsAndFields(useStatic: false, DeclaringCompilation, parameterList); } + else + { + var ctor = new SynthesizedPrimaryConstructor(this, syntax); - builder.PrimaryConstructor = ctor; + if (this.IsStatic) + { + diagnostics.Add(ErrorCode.ERR_ConstructorInStaticClass, syntax.Identifier.GetLocation()); + } - var compilation = DeclaringCompilation; - builder.UpdateIsNullableEnabledForConstructorsAndFields(ctor.IsStatic, compilation, parameterList); - if (syntax is { PrimaryConstructorBaseTypeIfClass: { ArgumentList: { } baseParamList } }) - { - builder.UpdateIsNullableEnabledForConstructorsAndFields(ctor.IsStatic, compilation, baseParamList); + builder.PrimaryConstructor = ctor; + + var compilation = DeclaringCompilation; + builder.UpdateIsNullableEnabledForConstructorsAndFields(ctor.IsStatic, compilation, parameterList); + if (syntax is { PrimaryConstructorBaseTypeIfClass: { ArgumentList: { } baseParamList } }) + { + builder.UpdateIsNullableEnabledForConstructorsAndFields(ctor.IsStatic, compilation, baseParamList); + } } } else @@ -4740,7 +4795,7 @@ private void CheckForStructBadInitializers(DeclaredMembersAndInitializersBuilder if (builder.DeclarationWithParameters is not null) { Debug.Assert(builder.DeclarationWithParameters is TypeDeclarationSyntax { ParameterList: not null } type - && type.Kind() is (SyntaxKind.RecordStructDeclaration or SyntaxKind.StructDeclaration)); + && type.Kind() is (SyntaxKind.RecordStructDeclaration or SyntaxKind.StructDeclaration or SyntaxKind.UnionDeclaration)); return; } @@ -4772,7 +4827,7 @@ private void AddSynthesizedSimpleProgramEntryPointIfNecessary(MembersAndInitiali private void AddSynthesizedTypeMembersIfNecessary(MembersAndInitializersBuilder builder, DeclaredMembersAndInitializers declaredMembersAndInitializers, BindingDiagnosticBag diagnostics) { - if (declaration.Kind is not (DeclarationKind.Record or DeclarationKind.RecordStruct) && declaredMembersAndInitializers.PrimaryConstructor is null) + if (declaration.Kind is not (DeclarationKind.Record or DeclarationKind.RecordStruct or DeclarationKind.Union) && declaredMembersAndInitializers.PrimaryConstructor is null) { return; } @@ -4780,7 +4835,7 @@ private void AddSynthesizedTypeMembersIfNecessary(MembersAndInitializersBuilder var membersSoFar = builder.GetNonTypeMembers(this, declaredMembersAndInitializers); var members = ArrayBuilder.GetInstance(membersSoFar.Count + 1); - if (declaration.Kind is not (DeclarationKind.Record or DeclarationKind.RecordStruct)) + if (declaration.Kind is not (DeclarationKind.Record or DeclarationKind.RecordStruct or DeclarationKind.Union)) { // primary ctor var ctor = declaredMembersAndInitializers.PrimaryConstructor; @@ -4795,6 +4850,87 @@ private void AddSynthesizedTypeMembersIfNecessary(MembersAndInitializersBuilder } Debug.Assert(declaredMembersAndInitializers.PrimaryConstructor?.GetBackingFields().Any() != true); + CSharpCompilation compilation = this.DeclaringCompilation; + + if (declaration.Kind is DeclarationKind.Union) + { + // Synthesize Value property + var valuePropertySyntax = (TypeDeclarationSyntax)declaration.Declarations[0].SyntaxReference.GetSyntax(); + var valueProperty = new SynthesizedUnionValuePropertySymbol(this, valuePropertySyntax, diagnostics); + members.Add(valueProperty); + Debug.Assert(valueProperty.GetMethod is object); + Debug.Assert(valueProperty.SetMethod is null); + members.Add(valueProperty.GetMethod); + var backingField = valueProperty.DeclaredBackingField; + Debug.Assert(backingField is object); + members.Add(backingField); + + builder.UpdateIsNullableEnabledForConstructorsAndFields(useStatic: false, value: true); + + bool report_ERR_UnionDeclarationNeedsCaseTypes = true; + if (declaredMembersAndInitializers.DeclarationWithParameters?.ParameterList is { } parameterList) + { + // Synthesize Union type constructors + + var binderFactory = compilation.GetBinderFactory(declaredMembersAndInitializers.DeclarationWithParameters.SyntaxTree); + var signatureBinder = binderFactory.GetBinder(parameterList, declaredMembersAndInitializers.DeclarationWithParameters, this); + + // Constraints are checked in AfterAddingTypeMembersChecks. + signatureBinder = signatureBinder.WithAdditionalFlagsAndContainingMemberOrLambda(BinderFlags.SuppressConstraintChecks, this); + var typesBuilder = ArrayBuilder.GetInstance(parameterList.Parameters.Count); + + foreach (var parameterSyntax in parameterList.Parameters) + { + report_ERR_UnionDeclarationNeedsCaseTypes = false; + + if (parameterSyntax.IsArgList) + { + // CS1669: __arglist is not valid in this context + diagnostics.Add(ErrorCode.ERR_IllegalVarArgs, parameterSyntax.Identifier.GetLocation()); + continue; + } + + // https://github.com/dotnet/roslyn/issues/82636: Complain for anything but a type syntax in the parameter syntax + + Debug.Assert(parameterSyntax.Type != null); + typesBuilder.Add(parameterSyntax.Type); + } + + int memberOffset = members.Count + typesBuilder.Count; // In order to keep the same relative order between constructors during emit we assign offset in decreasing order + foreach (var typeSyntax in typesBuilder) + { + var parameterType = signatureBinder.BindType(typeSyntax, diagnostics, suppressUseSiteDiagnostics: false); + + // Check that conversion to object is possible + + var useSiteInfo = signatureBinder.GetNewCompoundUseSiteInfo(diagnostics); + Conversion c = compilation.Conversions.ClassifyImplicitConversionFromType(parameterType.Type, compilation.GetSpecialType(SpecialType.System_Object), ref useSiteInfo); + diagnostics.Add(typeSyntax, useSiteInfo); + + if (!SynthesizedUnionCtor.IsValidParameterTypeConversion(c) && !parameterType.Type.IsErrorType()) + { + diagnostics.Add(ErrorCode.ERR_NoImplicitConversionToObject, typeSyntax, parameterType.Type); + } + + var ctor = new SynthesizedUnionCtor(this, typeSyntax.Location, parameterType, memberOffset: --memberOffset); + members.Add(ctor); + } + + Debug.Assert(memberOffset == members.Count - typesBuilder.Count); + + typesBuilder.Free(); + } + + if (report_ERR_UnionDeclarationNeedsCaseTypes) + { + diagnostics.Add(ErrorCode.ERR_UnionDeclarationNeedsCaseTypes, valuePropertySyntax.Identifier.GetLocation()); + } + + members.AddRange(membersSoFar); + builder.SetNonTypeMembers(members); + + return; + } ParameterListSyntax? paramList = declaredMembersAndInitializers.DeclarationWithParameters?.ParameterList; var memberSignatures = s_duplicateRecordMemberSignatureDictionary.Allocate(); @@ -4823,7 +4959,6 @@ private void AddSynthesizedTypeMembersIfNecessary(MembersAndInitializersBuilder } } - CSharpCompilation compilation = this.DeclaringCompilation; bool isRecordClass = declaration.Kind == DeclarationKind.Record; // Positional record @@ -5987,18 +6122,18 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r #region Extension Methods - internal bool ContainsExtensionMethods + internal bool ContainsExtensions { get { - if (!_lazyContainsExtensionMethods.HasValue()) + if (!_lazyContainsExtensions.HasValue()) { - bool containsExtensionMethods = ((this.IsStatic && !this.IsGenericType) || this.IsScriptClass) && + bool containsExtensions = ((this.IsStatic && !this.IsGenericType) || this.IsScriptClass) && (this.declaration.ContainsExtensionMethods || this.declaration.ContainsExtensionDeclarations); - _lazyContainsExtensionMethods = containsExtensionMethods.ToThreeState(); + _lazyContainsExtensions = containsExtensions.ToThreeState(); } - return _lazyContainsExtensionMethods.Value(); + return _lazyContainsExtensions.Value(); } } @@ -6016,11 +6151,11 @@ internal bool AnyMemberHasAttributes } } - public override bool MightContainExtensionMethods + public override bool MightContainExtensions { get { - return this.ContainsExtensionMethods; + return this.ContainsExtensions; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol_ImplementationChecks.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol_ImplementationChecks.cs index e63868863c35..4f85e4666de2 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol_ImplementationChecks.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberContainerSymbol_ImplementationChecks.cs @@ -989,6 +989,14 @@ void checkSingleOverriddenMember(Symbol overridingMember, Symbol overriddenMembe diagnostics, (diagnostics, overriddenEvent, overridingEvent, location) => diagnostics.Add(ErrorCode.WRN_NullabilityMismatchInTypeOnOverride, location), overridingMemberLocation); + + CheckCallerUnsafeMismatch( + implementedMember: null, + overridingEvent, + ErrorCode.ERR_CallerUnsafeOverridingSafe, + overridingMemberLocation, + static l => l, + diagnostics); } } else @@ -1204,6 +1212,14 @@ static void checkValidMethodOverride( }, overridingMemberLocation, invokedAsExtensionMethod: false); + + CheckCallerUnsafeMismatch( + implementedMember: null, + overridingMethod, + ErrorCode.ERR_CallerUnsafeOverridingSafe, + overridingMemberLocation, + static l => l, + diagnostics); } } @@ -1545,6 +1561,28 @@ internal static void CheckRefReadonlyInMismatch( } } } + + internal static void CheckCallerUnsafeMismatch( + Symbol? implementedMember, + Symbol? overridingMember, + ErrorCode errorCode, + TArg arg, + Func overridingMemberLocation, + BindingDiagnosticBag diagnostics) + { + if (overridingMember is null) + { + return; + } + + Debug.Assert(implementedMember is not null || overridingMember.IsDefinition); + + var leastOverriddenMember = implementedMember ?? overridingMember.GetLeastOverriddenMember(overridingMember.ContainingType); + if (overridingMember.CallerUnsafeMode == CallerUnsafeMode.Explicit && leastOverriddenMember.CallerUnsafeMode == CallerUnsafeMode.None) + { + diagnostics.Add(errorCode, overridingMemberLocation(arg), overridingMember, leastOverriddenMember); + } + } #nullable disable private static bool PerformValidNullableOverrideCheck( diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs index 6877e5eda7b8..88284d93e2de 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberFieldSymbol.cs @@ -589,10 +589,7 @@ private TypeAndRefKind GetTypeAndRefKind(ConsList fieldsBeingBound) diagnostics.Add(ErrorCode.ERR_IllegalFixedType, loc); } - if (!binder.InUnsafeRegion) - { - diagnosticsForFirstDeclarator.Add(ErrorCode.ERR_UnsafeNeeded, declarator.Location); - } + binder.ReportUnsafeIfNotAllowed(declarator.Location, diagnosticsForFirstDeclarator, disallowedUnder: MemorySafetyRules.Legacy); } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberMethodSymbol.cs index b24c62262b81..09d54d1cec8a 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberMethodSymbol.cs @@ -684,7 +684,7 @@ public sealed override bool IsStatic } } - internal bool IsUnsafe + internal sealed override bool IsUnsafe { get { @@ -982,6 +982,13 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions, compilation.EnsureIsReadOnlyAttributeExists(diagnostics, _location, modifyCompilation: true); } + if (NeedsSynthesizedRequiresUnsafeAttribute) + { + Debug.Assert(CallerUnsafeMode == CallerUnsafeMode.Explicit); + MessageID.IDS_FeatureUnsafeEvolution.CheckFeatureAvailability(diagnostics, compilation, _location); + Binder.GetWellKnownTypeMember(compilation, WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor, diagnostics, _location); + } + if (compilation.ShouldEmitNullableAttributes(this) && ShouldEmitNullableContextValue(out _)) { diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbol.cs index 74d087a84ca9..c9391c534f06 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbol.cs @@ -4,11 +4,9 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; using Microsoft.CodeAnalysis.CSharp.Emit; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.Symbols { @@ -95,6 +93,42 @@ static Location getLocation(ParameterSymbol parameter, Location location, bool i internal sealed override bool UseUpdatedEscapeRules => ContainingModule.UseUpdatedEscapeRules; + /// + /// Whether the method has the keyword in its signature. + /// Do not confuse with . + /// + internal abstract bool IsUnsafe { get; } + + internal bool HasRequiresUnsafeAttribute => GetDecodedWellKnownAttributeData()?.HasRequiresUnsafeAttribute == true; + + internal sealed override CallerUnsafeMode CallerUnsafeMode + { + get + { + if (ContainingModule.UseUpdatedMemorySafetyRules) + { + Debug.Assert(AssociatedSymbol?.CallerUnsafeMode != CallerUnsafeMode.Implicit); + + return HasRequiresUnsafeAttribute || IsExtern || AssociatedSymbol?.CallerUnsafeMode == CallerUnsafeMode.Explicit + ? CallerUnsafeMode.Explicit + : CallerUnsafeMode.None; + } + + return this.HasParameterContainingPointerType() || ReturnType.ContainsPointerOrFunctionPointer() + ? CallerUnsafeMode.Implicit : CallerUnsafeMode.None; + } + } + + protected bool NeedsSynthesizedRequiresUnsafeAttribute + { + get + { + return ContainingModule.UseUpdatedMemorySafetyRules && + !HasRequiresUnsafeAttribute && + (IsExtern || AssociatedSymbol?.IsExtern == true); + } + } + internal override bool HasAsyncMethodBuilderAttribute(out TypeSymbol? builderArgument) { return SourceMemberContainerTypeSymbol.HasAsyncMethodBuilderAttribute(this, out builderArgument); @@ -117,6 +151,12 @@ internal static void AddSynthesizedAttributes(MethodSymbol target, PEModuleBuild var compilation = target.DeclaringCompilation; + if (target is SourceMethodSymbol { NeedsSynthesizedRequiresUnsafeAttribute: true }) + { + Debug.Assert(target.CallerUnsafeMode == CallerUnsafeMode.Explicit); + AddSynthesizedAttribute(ref attributes, compilation.TrySynthesizeAttribute(WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor)); + } + if (compilation.ShouldEmitNullableAttributes(target) && target.ShouldEmitNullableContextValue(out byte nullableContextValue)) { diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs index 9070b2dc0ec8..7b0ed156ccfe 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceMethodSymbolWithAttributes.cs @@ -635,6 +635,26 @@ private void DecodeWellKnownAttributeAppliedToMethod(ref DecodeWellKnownAttribut diagnostics.Add(ErrorCode.ERR_UnscopedRefAttributeUnsupportedMemberTarget, arguments.AttributeSyntaxOpt.Location); } } + else if (attribute.IsTargetAttribute(AttributeDescription.RequiresUnsafeAttribute)) + { + if (this.MethodKind is MethodKind.AnonymousFunction or MethodKind.Destructor or MethodKind.LambdaMethod or MethodKind.StaticConstructor) + { + diagnostics.Add(ErrorCode.ERR_RequiresUnsafeAttributeUnsupportedMemberTarget, arguments.AttributeSyntaxOpt.Location); + } + else + { + if (ContainingModule.UseUpdatedMemorySafetyRules) + { + MessageID.IDS_FeatureUnsafeEvolution.CheckFeatureAvailability(diagnostics, arguments.AttributeSyntaxOpt); + } + else + { + diagnostics.Add(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, arguments.AttributeSyntaxOpt.Location); + } + + arguments.GetOrCreateData().HasRequiresUnsafeAttribute = true; + } + } else if (attribute.IsTargetAttribute(AttributeDescription.InterceptsLocationAttribute)) { DecodeInterceptsLocationAttribute(arguments); @@ -670,7 +690,7 @@ private void DecodeWellKnownAttributeAppliedToMethod(ref DecodeWellKnownAttribut } } - internal ThreeState IsRuntimeAsyncEnabledInMethod + internal override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => GetDecodedWellKnownAttributeData()?.RuntimeAsyncMethodGenerationSetting ?? ThreeState.Unknown; internal override ImmutableArray NotNullMembers => diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceModuleSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceModuleSymbol.cs index 76b71f265724..891fb1a04c27 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceModuleSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceModuleSymbol.cs @@ -253,6 +253,8 @@ internal override void ForceComplete(SourceLocation? locationOpt, Predicate /// Returns data decoded from attribute or null if there is no attribute. /// This property returns if attribute arguments haven't been decoded yet. diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol.cs index dbb87a989882..665fbe2668be 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol.cs @@ -79,6 +79,7 @@ internal SourceNamedTypeSymbol(NamespaceOrTypeSymbol containingSymbol, MergedTyp switch (declaration.Kind) { case DeclarationKind.Struct: + case DeclarationKind.Union: case DeclarationKind.Interface: case DeclarationKind.Enum: case DeclarationKind.Delegate: @@ -121,6 +122,7 @@ private static SyntaxToken GetName(CSharpSyntaxNode node) case SyntaxKind.ClassDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.RecordStructDeclaration: return ((BaseTypeDeclarationSyntax)node).Identifier; @@ -162,6 +164,7 @@ private ImmutableArray MakeTypeParameters(BindingDiagnostic { case SyntaxKind.ClassDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.RecordStructDeclaration: @@ -472,6 +475,7 @@ private static SyntaxList GetConstraintClau { case SyntaxKind.ClassDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.RecordStructDeclaration: @@ -806,7 +810,7 @@ IAttributeTargetSymbol IAttributeTargetSymbol.AttributesOwner AttributeLocation IAttributeTargetSymbol.DefaultAttributeLocation { - get { return AttributeLocation.Type; } + get { return IsExtension ? AttributeLocation.Extension : AttributeLocation.Type; } } AttributeLocation IAttributeTargetSymbol.AllowedAttributeLocations @@ -1048,6 +1052,21 @@ internal override (CSharpAttributeData?, BoundAttribute?) EarlyDecodeWellKnownAt return (null, null); } + if (CSharpAttributeData.IsTargetEarlyAttribute(arguments.AttributeType, arguments.AttributeSyntax, AttributeDescription.UnionAttribute)) + { + (attributeData, boundAttribute) = arguments.Binder.GetAttribute(arguments.AttributeSyntax, arguments.AttributeType, beforeAttributePartBound: null, afterAttributePartBound: null, out hasAnyDiagnostics); + if (!attributeData.HasErrors && attributeData.CommonConstructorArguments.IsEmpty) + { + arguments.GetOrCreateData().HasUnionAttribute = true; + if (!hasAnyDiagnostics) + { + return (attributeData, boundAttribute); + } + } + + return (null, null); + } + return base.EarlyDecodeWellKnownAttribute(ref arguments); } #nullable disable @@ -1441,6 +1460,22 @@ internal override bool HasCompilerLoweringPreserveAttribute } #nullable enable + internal override bool IsUnionTypeCore + { + get + { + return IsUnionDeclaration || HasUnionAttribute; + } + } + + private bool HasUnionAttribute + { + get + { + return GetEarlyDecodedWellKnownAttributeData()?.HasUnionAttribute == true; + } + } + internal sealed override bool IsInterpolatedStringHandlerType => GetEarlyDecodedWellKnownAttributeData()?.HasInterpolatedStringHandlerAttribute == true; #nullable disable @@ -1703,7 +1738,7 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r CSharpCompilation compilation = this.DeclaringCompilation; - if (this.ContainsExtensionMethods) + if (this.ContainsExtensions) { // No need to check if [Extension] attribute was explicitly set since // we'll issue CS1112 error in those cases and won't generate IL. @@ -1808,6 +1843,28 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r SynthesizedAttributeData.Create(DeclaringCompilation, parameterlessConstructor, arguments: [], namedArguments: [])); } } + + // Union type + if (ShouldApplyUnionAttribute()) + { + AddSynthesizedAttribute(ref attributes, compilation.TrySynthesizeAttribute(WellKnownMember.System_Runtime_CompilerServices_UnionAttribute__ctor)); + } + } + + private bool ShouldApplyUnionAttribute() + { + return IsUnionDeclaration && !HasUnionAttribute; + } + + protected override void AfterMembersChecks(BindingDiagnosticBag diagnostics) + { + base.AfterMembersChecks(diagnostics); + + // Union type + if (ShouldApplyUnionAttribute()) + { + _ = Binder.GetWellKnownTypeMember(DeclaringCompilation, WellKnownMember.System_Runtime_CompilerServices_UnionAttribute__ctor, diagnostics, GetFirstLocation()); + } } #endregion diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol_Bases.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol_Bases.cs index 833bed513592..e9c46bc0f9b8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol_Bases.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamedTypeSymbol_Bases.cs @@ -367,14 +367,20 @@ static bool containsOnlyOblivious(TypeSymbol type) CompoundUseSiteInfo useSiteInfo = new CompoundUseSiteInfo(diagnostics, ContainingAssembly); - if (declaration.Kind is DeclarationKind.Record or DeclarationKind.RecordStruct) + switch (declaration.Kind) { - var type = DeclaringCompilation.GetWellKnownType(WellKnownType.System_IEquatable_T).Construct(this); - if (baseInterfaces.IndexOf(type, SymbolEqualityComparer.AllIgnoreOptions) < 0) - { - baseInterfaces.Add(type); - type.AddUseSiteInfo(ref useSiteInfo); - } + case DeclarationKind.Record or DeclarationKind.RecordStruct: + { + var type = DeclaringCompilation.GetWellKnownType(WellKnownType.System_IEquatable_T).Construct(this); + addImplicitlyImplementedInterface(baseInterfaces, type); + } + break; + case DeclarationKind.Union: + { + var type = DeclaringCompilation.GetWellKnownType(WellKnownType.System_Runtime_CompilerServices_IUnion); + addImplicitlyImplementedInterface(baseInterfaces, type); + } + break; } if ((object)baseType != null) @@ -469,6 +475,14 @@ static bool containsOnlyOblivious(TypeSymbol type) diagnostics.Add(GetFirstLocation(), useSiteInfo); return new Tuple>(baseType, baseInterfacesRO); + + static void addImplicitlyImplementedInterface(ArrayBuilder baseInterfaces, NamedTypeSymbol type) + { + if (baseInterfaces.IndexOf(type, SymbolEqualityComparer.AllIgnoreOptions) < 0) + { + baseInterfaces.Add(type); + } + } } private static BaseListSyntax GetBaseListOpt(SingleTypeDeclaration decl) diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamespaceSymbol.AliasesAndUsings.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamespaceSymbol.AliasesAndUsings.cs index 7ed57184b571..caab6d3d297c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamespaceSymbol.AliasesAndUsings.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamespaceSymbol.AliasesAndUsings.cs @@ -641,9 +641,10 @@ UsingsAndDiagnostics buildUsings( ImmutableDictionary.Builder? usingAliasesMap = null; ArrayBuilder? usingAliases = null; - // A binder that contains the extern aliases but not the usings. The resolution of the target of a using directive or alias + // Binders that contain the extern aliases but not the usings. The resolution of the target of a using directive or alias // should not make use of other peer usings. - Binder? declarationBinder = null; + Binder? declarationBinderSafe = null; + Binder? declarationBinderUnsafe = null; // with UnsafeRegion flag PooledHashSet? uniqueUsings = null; PooledHashSet? uniqueGlobalUsings = null; @@ -732,7 +733,7 @@ UsingsAndDiagnostics buildUsings( continue; } - var flags = BinderFlags.SuppressConstraintChecks; + bool needsUnsafeBinder = false; if (usingDirective.UnsafeKeyword != default) { var unsafeKeywordLocation = usingDirective.UnsafeKeyword.GetLocation(); @@ -746,7 +747,7 @@ UsingsAndDiagnostics buildUsings( declaringSymbol.CheckUnsafeModifier(DeclarationModifiers.Unsafe, unsafeKeywordLocation, diagnostics); } - flags |= BinderFlags.UnsafeRegion; + needsUnsafeBinder = true; } else { @@ -755,14 +756,26 @@ UsingsAndDiagnostics buildUsings( // List;` to be written. In 12.0 and onwards though, we require the code to // explicitly contain the `unsafe` keyword. if (!compilation.IsFeatureEnabled(MessageID.IDS_FeatureUsingTypeAlias)) - flags |= BinderFlags.UnsafeRegion; + needsUnsafeBinder = true; } var directiveDiagnostics = BindingDiagnosticBag.GetInstance(); Debug.Assert(directiveDiagnostics.DiagnosticBag is object); Debug.Assert(directiveDiagnostics.DependenciesBag is object); - declarationBinder ??= compilation.GetBinderFactory(declarationSyntax.SyntaxTree).GetBinder(usingDirective.NamespaceOrType).WithAdditionalFlags(flags); + declarationBinderSafe ??= compilation.GetBinderFactory(declarationSyntax.SyntaxTree).GetBinder(usingDirective.NamespaceOrType).WithAdditionalFlags(BinderFlags.SuppressConstraintChecks); + + Binder declarationBinder; + if (needsUnsafeBinder) + { + declarationBinderUnsafe ??= declarationBinderSafe.WithAdditionalFlags(BinderFlags.UnsafeRegion); + declarationBinder = declarationBinderUnsafe; + } + else + { + declarationBinder = declarationBinderSafe; + } + var imported = declarationBinder.BindNamespaceOrTypeSymbol(usingDirective.NamespaceOrType, directiveDiagnostics, basesBeingResolved).NamespaceOrTypeSymbol; bool addDirectiveDiagnostics = true; @@ -810,6 +823,7 @@ UsingsAndDiagnostics buildUsings( else { declarationBinder.ReportDiagnosticsIfObsolete(diagnostics, importedType, usingDirective.NamespaceOrType, hasBaseReceiver: false); + declarationBinder.AssertNotUnsafeMemberAccess(importedType); getOrCreateUsingsBuilder(ref usings, globalUsingNamespacesOrTypes).Add(new NamespaceOrTypeAndUsingDirective(importedType, usingDirective, directiveDiagnostics.DependenciesBag.ToImmutableArray())); } @@ -991,8 +1005,15 @@ private static void Validate(SourceNamespaceSymbol declaringSymbol, SyntaxRefere alias.Alias.CheckConstraints(diagnostics); + Debug.Assert(alias.UsingDirective != null); + + if (alias.UsingDirective.UnsafeKeyword == default) + { + compilation.GetBinder(alias.UsingDirective.NamespaceOrType).ReportDiagnosticsIfUnsafeMemberAccess(diagnostics.DiagnosticBag, alias.Alias.Target, alias.Alias.GetFirstLocation()); + } + semanticDiagnostics.AddRange(diagnostics.DiagnosticBag); - recordImportDependencies(alias.UsingDirective!, target); + recordImportDependencies(alias.UsingDirective, target); } } @@ -1019,6 +1040,11 @@ private static void Validate(SourceNamespaceSymbol declaringSymbol, SyntaxRefere var typeSymbol = (TypeSymbol)target; var location = usingDirective.NamespaceOrType.Location; typeSymbol.CheckAllConstraints(compilation, conversions, location, diagnostics); + + if (usingDirective.UnsafeKeyword == default) + { + compilation.GetBinder(usingDirective.NamespaceOrType).ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, typeSymbol, usingDirective.NamespaceOrType); + } } semanticDiagnostics.AddRange(diagnostics.DiagnosticBag); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamespaceSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamespaceSymbol.cs index 4663544e1fc3..d00ec5773fd5 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamespaceSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceNamespaceSymbol.cs @@ -407,6 +407,7 @@ private NamespaceOrTypeSymbol BuildSymbol(MergedNamespaceOrTypeDeclaration decla return new SourceNamespaceSymbol(_module, this, (MergedNamespaceDeclaration)declaration, diagnostics); case DeclarationKind.Struct: + case DeclarationKind.Union: case DeclarationKind.Interface: case DeclarationKind.Enum: case DeclarationKind.Delegate: diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs index a42ea9e1210d..7c3c5b42601f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertyAccessorSymbol.cs @@ -126,6 +126,31 @@ public static SourcePropertyAccessorSymbol CreateAccessorSymbol( syntax, diagnostics); } + + public static SourcePropertyAccessorSymbol CreateAccessorSymbol( + NamedTypeSymbol containingType, + SynthesizedUnionValuePropertySymbol property, + DeclarationModifiers propertyModifiers, + Location location, + CSharpSyntaxNode syntax, + BindingDiagnosticBag diagnostics) + { + return new SourcePropertyAccessorSymbol( + containingType, + property, + propertyModifiers, + location, + syntax, + hasBlockBody: false, + hasExpressionBody: false, + isIterator: false, + modifiers: default, + MethodKind.PropertyGet, + usesInit: false, + isAutoPropertyAccessor: true, + isNullableAnalysisEnabled: false, + diagnostics); + } #nullable disable internal sealed override ImmutableArray NotNullMembers @@ -254,6 +279,11 @@ private static DeclarationModifiers GetAccessorModifiers(DeclarationModifiers pr internal override ExecutableCodeBinder TryGetBodyBinder(BinderFactory binderFactoryOpt = null, bool ignoreAccessibility = false) { + if (_property is SynthesizedUnionValuePropertySymbol or SynthesizedRecordEqualityContractProperty or SynthesizedRecordPropertySymbol) + { + return null; + } + return TryGetBodyBinderFromSyntax(binderFactoryOpt, ignoreAccessibility); } @@ -499,6 +529,7 @@ private static DeclarationModifiers MakeModifiers(NamedTypeSymbol containingType // Check that the set of modifiers is allowed var allowedModifiers = isExplicitInterfaceImplementation ? DeclarationModifiers.None : DeclarationModifiers.AccessibilityMask; + if (containingType.IsStructType()) { allowedModifiers |= DeclarationModifiers.ReadOnly; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs index da8fd2182e03..d90601fde52c 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbol.cs @@ -771,7 +771,7 @@ private void PartialPropertyChecks(SourcePropertySymbol implementation, BindingD diagnostics.Add(ErrorCode.ERR_PartialMemberReadOnlyDifference, implementation.GetFirstLocation()); } - if ((_modifiers & DeclarationModifiers.Unsafe) != (implementation._modifiers & DeclarationModifiers.Unsafe) && this.CompilationAllowsUnsafe()) // Don't cascade. + if (IsDeclaredUnsafe != implementation.IsDeclaredUnsafe && this.CompilationAllowsUnsafe()) // Don't cascade. { diagnostics.Add(ErrorCode.ERR_PartialMemberUnsafeDifference, implementation.GetFirstLocation()); } @@ -822,6 +822,27 @@ private void PartialPropertyChecks(SourcePropertySymbol implementation, BindingD private static BaseParameterListSyntax? GetParameterListSyntax(CSharpSyntaxNode syntax) => (syntax as IndexerDeclarationSyntax)?.ParameterList; + /// + /// Whether the property has the keyword in its signature. + /// + private bool IsDeclaredUnsafe => (_modifiers & DeclarationModifiers.Unsafe) != 0; + + internal override CallerUnsafeMode CallerUnsafeMode + { + get + { + if (ContainingModule.UseUpdatedMemorySafetyRules) + { + return HasRequiresUnsafeAttribute || IsExtern + ? CallerUnsafeMode.Explicit + : CallerUnsafeMode.None; + } + + return this.HasParameterContainingPointerType() || Type.ContainsPointerOrFunctionPointer() + ? CallerUnsafeMode.Implicit : CallerUnsafeMode.None; + } + } + public sealed override bool IsExtern => PartialImplementationPart is { } implementation ? implementation.IsExtern : HasExternModifier; internal SourcePropertySymbol? OtherPartOfPartial => _otherPartOfPartial; diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs index 851134fb7bdc..57c2e8c585ea 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourcePropertySymbolBase.cs @@ -1016,10 +1016,11 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions, #nullable disable - Location location = TypeLocation; + Location typeLocation = TypeLocation; + var location = GetFirstLocation(); var compilation = DeclaringCompilation; - Debug.Assert(location != null); + Debug.Assert(typeLocation != null); // Check constraints on return type and parameters. Note: Dev10 uses the // property name location for any such errors. We'll do the same for return @@ -1040,7 +1041,14 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions, if (_refKind == RefKind.RefReadOnly) { - compilation.EnsureIsReadOnlyAttributeExists(diagnostics, location, modifyCompilation: true); + compilation.EnsureIsReadOnlyAttributeExists(diagnostics, typeLocation, modifyCompilation: true); + } + + if (NeedsSynthesizedRequiresUnsafeAttribute) + { + Debug.Assert(CallerUnsafeMode == CallerUnsafeMode.Explicit); + MessageID.IDS_FeatureUnsafeEvolution.CheckFeatureAvailability(diagnostics, compilation, location); + Binder.GetWellKnownTypeMember(compilation, WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor, diagnostics, location); } ParameterHelpers.EnsureRefKindAttributesExist(compilation, Parameters, diagnostics, modifyCompilation: true); @@ -1048,7 +1056,7 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions, if (compilation.ShouldEmitNativeIntegerAttributes(Type)) { - compilation.EnsureNativeIntegerAttributeExists(diagnostics, location, modifyCompilation: true); + compilation.EnsureNativeIntegerAttributeExists(diagnostics, typeLocation, modifyCompilation: true); } ParameterHelpers.EnsureNativeIntegerAttributeExists(compilation, Parameters, diagnostics, modifyCompilation: true); @@ -1058,7 +1066,7 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions, if (compilation.ShouldEmitNullableAttributes(this) && this.TypeWithAnnotations.NeedsNullableAttribute()) { - compilation.EnsureNullableAttributeExists(diagnostics, location, modifyCompilation: true); + compilation.EnsureNullableAttributeExists(diagnostics, typeLocation, modifyCompilation: true); } ParameterHelpers.EnsureNullableAttributeExists(compilation, this, Parameters, diagnostics, modifyCompilation: true); @@ -1067,7 +1075,7 @@ internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions, { ParameterHelpers.CheckUnderspecifiedGenericExtension(this, Parameters, diagnostics); - compilation.EnsureExtensionMarkerAttributeExists(diagnostics, GetFirstLocation(), modifyCompilation: true); + compilation.EnsureExtensionMarkerAttributeExists(diagnostics, location, modifyCompilation: true); } } @@ -1370,6 +1378,18 @@ private PropertyWellKnownAttributeData GetDecodedWellKnownAttributeData() return (PropertyWellKnownAttributeData)attributesBag.DecodedWellKnownAttributeData; } + internal bool HasRequiresUnsafeAttribute => GetDecodedWellKnownAttributeData()?.HasRequiresUnsafeAttribute == true; + + private bool NeedsSynthesizedRequiresUnsafeAttribute + { + get + { + return ContainingModule.UseUpdatedMemorySafetyRules && + !HasRequiresUnsafeAttribute && + IsExtern; + } + } + /// /// Returns data decoded from special early bound well-known attributes applied to the symbol or null if there are no applied attributes. /// @@ -1421,6 +1441,12 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r AddSynthesizedAttribute(ref attributes, moduleBuilder.SynthesizeIsReadOnlyAttribute(this)); } + if (NeedsSynthesizedRequiresUnsafeAttribute) + { + Debug.Assert(CallerUnsafeMode == CallerUnsafeMode.Explicit); + AddSynthesizedAttribute(ref attributes, compilation.TrySynthesizeAttribute(WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor)); + } + if (IsRequired) { AddSynthesizedAttribute( @@ -1615,6 +1641,11 @@ protected override void DecodeWellKnownAttributeImpl(ref DecodeWellKnownAttribut diagnostics.Add(ErrorCode.ERR_UnscopedRefAttributeUnsupportedMemberTarget, arguments.AttributeSyntaxOpt.Location); } } + else if (attribute.IsTargetAttribute(AttributeDescription.RequiresUnsafeAttribute)) + { + if (ContainingModule.UseUpdatedMemorySafetyRules) MessageID.IDS_FeatureUnsafeEvolution.CheckFeatureAvailability(diagnostics, arguments.AttributeSyntaxOpt); + arguments.GetOrCreateData().HasRequiresUnsafeAttribute = true; + } else if (attribute.IsTargetAttribute(AttributeDescription.OverloadResolutionPriorityAttribute)) { MessageID.IDS_FeatureOverloadResolutionPriority.CheckFeatureAvailability(diagnostics, arguments.AttributeSyntaxOpt); diff --git a/src/Compilers/CSharp/Portable/Symbols/Source/SourceTypeParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Source/SourceTypeParameterSymbol.cs index 59a5d09a3afc..f0ca3b5cd9a4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Source/SourceTypeParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Source/SourceTypeParameterSymbol.cs @@ -401,29 +401,6 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r } } - internal byte GetSynthesizedNullableAttributeValue() - { - if (this.HasReferenceTypeConstraint) - { - switch (this.ReferenceTypeConstraintIsNullable) - { - case true: - return NullableAnnotationExtensions.AnnotatedAttributeValue; - case false: - return NullableAnnotationExtensions.NotAnnotatedAttributeValue; - } - } - else if (this.HasNotNullConstraint) - { - return NullableAnnotationExtensions.NotAnnotatedAttributeValue; - } - else if (!this.HasValueTypeConstraint && this.ConstraintTypesNoUseSiteDiagnostics.IsEmpty && this.IsNotNullable == false) - { - return NullableAnnotationExtensions.AnnotatedAttributeValue; - } - return NullableAnnotationExtensions.ObliviousAttributeValue; - } - protected sealed override void DecodeWellKnownAttributeImpl(ref DecodeWellKnownAttributeArguments arguments) { Debug.Assert((object)arguments.AttributeSyntaxOpt != null); diff --git a/src/Compilers/CSharp/Portable/Symbols/SubstitutedMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/SubstitutedMethodSymbol.cs index f75b0c8e5766..7e63ff81d025 100644 --- a/src/Compilers/CSharp/Portable/Symbols/SubstitutedMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/SubstitutedMethodSymbol.cs @@ -171,6 +171,10 @@ public override TypeSymbol ReceiverType } } + public override bool IsAsync => OriginalDefinition.IsAsync; + + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); + public override TypeWithAnnotations GetTypeInferredDuringReduction(TypeParameterSymbol reducedFromTypeParameter) { // This will throw if API shouldn't be supported or there is a problem with the argument. diff --git a/src/Compilers/CSharp/Portable/Symbols/SubstitutedNamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/SubstitutedNamedTypeSymbol.cs index 66b1aa2c472f..2f0cf7477ae9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/SubstitutedNamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/SubstitutedNamedTypeSymbol.cs @@ -494,6 +494,8 @@ internal sealed override bool HasInlineArrayAttribute(out int length) internal sealed override bool HasCompilerLoweringPreserveAttribute => _underlyingType.HasCompilerLoweringPreserveAttribute; + internal override bool IsUnionTypeCore => _underlyingType.IsUnionTypeCore; + #nullable enable internal sealed override ParameterSymbol? ExtensionParameter { diff --git a/src/Compilers/CSharp/Portable/Symbols/Symbol.cs b/src/Compilers/CSharp/Portable/Symbols/Symbol.cs index ace4b2cbc1bb..3acd1f6ff6b4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Symbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Symbol.cs @@ -19,7 +19,6 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Symbols; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp { @@ -619,6 +618,13 @@ public virtual bool IsImplicitlyDeclared get { return false; } } + // https://github.com/dotnet/roslyn/issues/82546: add a public API for this (probably just expose a bool) + /// + /// Whether this member is considered unsafe under the updated memory safety rules. + /// See for more details. + /// + internal abstract CallerUnsafeMode CallerUnsafeMode { get; } + /// /// Returns true if this symbol can be referenced by its name in code. Examples of symbols /// that cannot be referenced by name are: @@ -1531,7 +1537,8 @@ internal enum ReservedAttributes RefSafetyRulesAttribute = 1 << 13, RequiresLocationAttribute = 1 << 14, ExtensionMarkerAttribute = 1 << 15, - ClosedAttribute = 1 << 16, + MemorySafetyRulesAttribute = 1 << 16, + ClosedAttribute = 1 << 17, } // PROTOTYPE(cc): Remove unnecessary 'permitted' flags from call sites of this method @@ -1609,6 +1616,10 @@ internal bool ReportExplicitUseOfReservedAttributes(in DecodeWellKnownAttributeA reportExplicitUseOfReservedAttribute(attribute, arguments, AttributeDescription.RefSafetyRulesAttribute)) { } + else if ((permitted & ReservedAttributes.MemorySafetyRulesAttribute) == 0 && + reportExplicitUseOfReservedAttribute(attribute, arguments, AttributeDescription.MemorySafetyRulesAttribute)) + { + } else if ((permitted & ReservedAttributes.ExtensionMarkerAttribute) == 0 && reportExplicitUseOfReservedAttribute(attribute, arguments, AttributeDescription.ExtensionMarkerAttribute)) { @@ -1690,13 +1701,11 @@ internal void GetCommonNullableValues(CSharpCompilation compilation, ref MostCom builder.AddValue(((ParameterSymbol)this).TypeWithAnnotations); break; case SymbolKind.TypeParameter: - if (this is SourceTypeParameterSymbol typeParameter) + var typeParameter = (TypeParameterSymbol)this; + builder.AddValue(typeParameter.GetSynthesizedNullableAttributeValue()); + foreach (var constraintType in typeParameter.ConstraintTypesNoUseSiteDiagnostics) { - builder.AddValue(typeParameter.GetSynthesizedNullableAttributeValue()); - foreach (var constraintType in typeParameter.ConstraintTypesNoUseSiteDiagnostics) - { - builder.AddValue(constraintType); - } + builder.AddValue(constraintType); } break; } diff --git a/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs b/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs index b0f844cfa2d5..854c3661482d 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Symbol_Attributes.cs @@ -428,6 +428,7 @@ internal bool LoadAndValidateAttributes( { Binder.CheckRequiredMembersInObjectInitializer(ctor, ImmutableArray.CastUp(boundAttribute.NamedArguments), boundAttribute.Syntax, diagnostics); attributeBinder.ReportDiagnosticsIfObsolete(diagnostics, ctor, boundAttribute.Syntax, hasBaseReceiver: false); + attributeBinder.ReportDiagnosticsIfUnsafeMemberAccess(diagnostics, ctor, boundAttribute.Syntax); } NullableWalker.AnalyzeIfNeeded(attributeBinder, boundAttribute, boundAttribute.Syntax, diagnostics.DiagnosticBag); } @@ -609,7 +610,7 @@ private ImmutableArray GetAttributesToBind( foreach (var attributeDeclarationSyntax in attributeDeclarationSyntaxList) { // We bind the attribute only if it has a matching target for the given ownerSymbol and attributeLocation. - if (MatchAttributeTarget(attributeTarget, symbolPart, attributeDeclarationSyntax.Target, diagnostics) && + if (MatchAttributeTarget(attributeTarget, symbolPart, attributeDeclarationSyntax, diagnostics) && ShouldBindAttributes(attributeDeclarationSyntax, diagnostics)) { if (syntaxBuilder == null) @@ -679,14 +680,22 @@ protected Binder GetAttributeBinder(SyntaxList attributeDec } #nullable disable - private static bool MatchAttributeTarget(IAttributeTargetSymbol attributeTarget, AttributeLocation symbolPart, AttributeTargetSpecifierSyntax targetOpt, BindingDiagnosticBag diagnostics) + private static bool MatchAttributeTarget(IAttributeTargetSymbol attributeTarget, AttributeLocation symbolPart, AttributeListSyntax attributeList, BindingDiagnosticBag diagnostics) { + AttributeLocation defaultAttributeLocation = attributeTarget.DefaultAttributeLocation; + if (defaultAttributeLocation == AttributeLocation.Extension) + { + diagnostics.Add(ErrorCode.ERR_AttributesNotAllowed, attributeList); + return false; + } + IAttributeTargetSymbol attributesOwner = attributeTarget.AttributesOwner; // Determine if the target symbol owns the attribute declaration. // We need to report diagnostics only once, so do it when visiting attributes for the owner. bool isOwner = symbolPart == AttributeLocation.None && ReferenceEquals(attributesOwner, attributeTarget); + AttributeTargetSpecifierSyntax targetOpt = attributeList.Target; if (targetOpt == null) { // only attributes with an explicit target match if the symbol doesn't own the attributes: @@ -703,7 +712,6 @@ private static bool MatchAttributeTarget(IAttributeTargetSymbol attributeTarget, } AttributeLocation allowedTargets = attributesOwner.AllowedAttributeLocations; - AttributeLocation explicitTarget = targetOpt.GetAttributeLocation(); if (explicitTarget == AttributeLocation.None) { @@ -725,7 +733,7 @@ private static bool MatchAttributeTarget(IAttributeTargetSymbol attributeTarget, { if (allowedTargets == AttributeLocation.None) { - switch (attributeTarget.DefaultAttributeLocation) + switch (defaultAttributeLocation) { case AttributeLocation.Assembly: case AttributeLocation.Module: @@ -900,6 +908,12 @@ private bool ValidateAttributeUsage( NamedTypeSymbol attributeType = attribute.AttributeClass; AttributeUsageInfo attributeUsageInfo = attributeType.GetAttributeUsageInfo(); + if (this is NamedTypeSymbol { IsExtension: true }) + { + diagnostics.Add(ErrorCode.ERR_AttributesNotAllowed, node.Name.Location); + return false; + } + // Given attribute can't be specified more than once if AllowMultiple is false. if (!uniqueAttributeTypes.Add(attributeType.OriginalDefinition) && !attributeUsageInfo.AllowMultiple) { diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListEnumeratorTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListEnumeratorTypeSymbol.cs index 34f74f82fcbb..1611eecb7b92 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListEnumeratorTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListEnumeratorTypeSymbol.cs @@ -132,7 +132,7 @@ static void addProperty(ArrayBuilder builder, PropertySymbol property) public override NamedTypeSymbol ConstructedFrom => this; - public override bool MightContainExtensionMethods => false; + public override bool MightContainExtensions => false; public override string Name => "Enumerator"; @@ -186,6 +186,8 @@ static void addProperty(ArrayBuilder builder, PropertySymbol property) internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal override bool IsInterpolatedStringHandlerType => false; internal sealed override ParameterSymbol? ExtensionParameter => null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListProperty.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListProperty.cs index 406c84d246d2..7a47f6803f68 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListProperty.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListProperty.cs @@ -76,6 +76,8 @@ internal SynthesizedReadOnlyListProperty( internal override bool HasUnscopedRefAttribute => false; + internal override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal override ObsoleteAttributeData? ObsoleteAttributeData => null; internal override int TryGetOverloadResolutionPriority() => 0; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListTypeSymbol.cs index fcbd74f42f18..2785e8c775e0 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/ReadOnlyListType/SynthesizedReadOnlyListTypeSymbol.cs @@ -845,7 +845,7 @@ public static bool CanCreateSingleElement(CSharpCompilation compilation) public override NamedTypeSymbol ConstructedFrom => this; - public override bool MightContainExtensionMethods => false; + public override bool MightContainExtensions => false; public override string Name { get; } @@ -899,6 +899,8 @@ public static bool CanCreateSingleElement(CSharpCompilation compilation) internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal override bool IsInterpolatedStringHandlerType => false; internal sealed override ParameterSymbol? ExtensionParameter => null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs index 2fe068f8f744..0cc42b4ee7de 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordEqualityContractProperty.cs @@ -49,6 +49,8 @@ public SynthesizedRecordEqualityContractProperty(SourceMemberContainerTypeSymbol public override bool IsImplicitlyDeclared => true; + internal override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + public override ImmutableArray DeclaringSyntaxReferences => ImmutableArray.Empty; public override OneOrMany> GetAttributeDeclarations() diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPropertySymbol.cs index 5c9c0c277698..feb9332f8ee3 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/Records/SynthesizedRecordPropertySymbol.cs @@ -50,6 +50,8 @@ public SynthesizedRecordPropertySymbol( public override IAttributeTargetSymbol AttributesOwner => BackingParameter as IAttributeTargetSymbol ?? this; + internal override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + protected override Location TypeLocation => ((ParameterSyntax)CSharpSyntaxNode).Type!.Location; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedBackingFieldSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedBackingFieldSymbol.cs index 4f5b4603fc00..78ef3fcc9724 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedBackingFieldSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedBackingFieldSymbol.cs @@ -119,6 +119,9 @@ public override Symbol AssociatedSymbol public override ImmutableArray Locations => _property.Locations; + public override Location TryGetFirstLocation() + => _property.TryGetFirstLocation(); + public override RefKind RefKind => _property.RefKind; public override ImmutableArray RefCustomModifiers => _property.RefCustomModifiers; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedCollectionBuilderProjectedMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedCollectionBuilderProjectedMethodSymbol.cs index 8ddc66e2b87b..5e525e9740f8 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedCollectionBuilderProjectedMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedCollectionBuilderProjectedMethodSymbol.cs @@ -43,8 +43,10 @@ public override ImmutableArray GetAttributes() => this.UnderlyingMethod.GetAttributes(); public override Symbol ContainingSymbol => this.UnderlyingMethod.ContainingSymbol; + public override bool IsAsync => this.UnderlyingMethod.IsAsync; public override ImmutableArray RefCustomModifiers => this.UnderlyingMethod.RefCustomModifiers; public override TypeWithAnnotations ReturnTypeWithAnnotations => this.UnderlyingMethod.ReturnTypeWithAnnotations; + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); /// /// The projection method itself is intentionally not obsolete. We don't want to report obsoletion errors when @@ -61,6 +63,11 @@ public override ImmutableArray GetAttributes() /// internal override UnmanagedCallersOnlyAttributeData? GetUnmanagedCallersOnlyAttributeData(bool forceComplete) => null; + /// + /// Similarly to , we report caller-unsafe errors on the instead. + /// + internal override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + // Note: it is very intentional that we return empty arrays for Type arguments/parameters. Consider a // hypothetical signature like: // @@ -134,4 +141,3 @@ private sealed class SynthesizedCollectionBuilderProjectedParameterSymbol( internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, ref ArrayBuilder attributes) => throw ExceptionUtilities.Unreachable(); } } - diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedContainer.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedContainer.cs index 383de1aef813..d806e828efd9 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedContainer.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedContainer.cs @@ -102,6 +102,8 @@ internal override ImmutableArray TypeArgumentsWithAnnotatio internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal sealed override bool IsInterpolatedStringHandlerType => false; internal sealed override bool HasDeclaredRequiredMembers => false; @@ -167,7 +169,7 @@ internal override IEnumerable GetFieldsToEmit() internal override ImmutableArray GetDeclaredInterfaces(ConsList basesBeingResolved) => InterfacesNoUseSiteDiagnostics(basesBeingResolved); - public override bool MightContainExtensionMethods => false; + public override bool MightContainExtensions => false; public override int Arity => TypeParameters.Length; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedAttributeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedAttributeSymbol.cs index 308f0745ee3d..19be3cbded80 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedAttributeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedAttributeSymbol.cs @@ -62,7 +62,7 @@ public SynthesizedEmbeddedAttributeSymbolBase( public override NamedTypeSymbol ConstructedFrom => this; - public override bool MightContainExtensionMethods => false; + public override bool MightContainExtensions => false; public override string Name => _name; @@ -115,6 +115,8 @@ internal override bool GetGuidString(out string guidString) internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal override bool IsInterpolatedStringHandlerType => false; #nullable enable diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedExtensionMarkerNameAttributeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedExtensionMarkerNameAttributeSymbol.cs index 52fb05b9c0d3..d730c001287f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedExtensionMarkerNameAttributeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedExtensionMarkerNameAttributeSymbol.cs @@ -6,8 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Reflection; -using Microsoft.Cci; using Microsoft.CodeAnalysis.PooledObjects; namespace Microsoft.CodeAnalysis.CSharp.Symbols @@ -16,7 +14,7 @@ internal sealed class SynthesizedEmbeddedExtensionMarkerAttributeSymbol : Synthe { private readonly ImmutableArray _constructors; private readonly SynthesizedFieldSymbol _nameField; - private readonly NamePropertySymbol _nameProperty; + private readonly SynthesizedPropertySymbol _nameProperty; private const string PropertyName = "Name"; private const string FieldName = "k__BackingField"; @@ -32,7 +30,7 @@ public SynthesizedEmbeddedExtensionMarkerAttributeSymbol( Debug.Assert(FieldName == GeneratedNames.MakeBackingFieldName(PropertyName)); _nameField = new SynthesizedFieldSymbol(this, systemStringType, FieldName, isReadOnly: true); - _nameProperty = new NamePropertySymbol(_nameField); + _nameProperty = new SynthesizedPropertySymbol(PropertyName, _nameField); _constructors = [new SynthesizedEmbeddedAttributeConstructorWithBodySymbol(this, getConstructorParameters, getConstructorBody)]; // Ensure we never get out of sync with the description @@ -83,116 +81,5 @@ public override ImmutableArray GetMembers(string name) public override IEnumerable MemberNames => [FieldName, PropertyName, WellKnownMemberNames.InstanceConstructorName]; - - private sealed class NamePropertySymbol : PropertySymbol - { - internal readonly SynthesizedFieldSymbol _backingField; - - public NamePropertySymbol(SynthesizedFieldSymbol backingField) - { - _backingField = backingField; - GetMethod = new NameGetAccessorMethodSymbol(this); - } - - public override string Name => PropertyName; - public override TypeWithAnnotations TypeWithAnnotations => _backingField.TypeWithAnnotations; - public override RefKind RefKind => RefKind.None; - public override ImmutableArray RefCustomModifiers => []; - public override MethodSymbol GetMethod { get; } - public override MethodSymbol? SetMethod => null; - public override Symbol ContainingSymbol => _backingField.ContainingSymbol; - public override Accessibility DeclaredAccessibility => Accessibility.Public; - - public override ImmutableArray Locations => []; - public override ImmutableArray DeclaringSyntaxReferences => []; - public override ImmutableArray ExplicitInterfaceImplementations => []; - public override ImmutableArray Parameters => []; - public override bool IsIndexer => false; - public override bool IsStatic => false; - public override bool IsVirtual => false; - public override bool IsOverride => false; - public override bool IsAbstract => false; - public override bool IsSealed => false; - public override bool IsExtern => false; - internal override bool IsRequired => false; - internal override bool HasSpecialName => false; - internal override CallingConvention CallingConvention => CallingConvention.HasThis; - internal override bool MustCallMethodsDirectly => false; - internal override bool HasUnscopedRefAttribute => false; - internal override ObsoleteAttributeData? ObsoleteAttributeData => null; - internal override int TryGetOverloadResolutionPriority() => 0; - } - - private sealed partial class NameGetAccessorMethodSymbol : SynthesizedMethodSymbol - { - private readonly NamePropertySymbol _nameProperty; - - public NameGetAccessorMethodSymbol(NamePropertySymbol nameProperty) - { - _nameProperty = nameProperty; - } - - public override string Name => "get_Name"; - internal override bool HasSpecialName => true; - public override MethodKind MethodKind => MethodKind.PropertyGet; - public override Symbol AssociatedSymbol => _nameProperty; - public override Symbol ContainingSymbol => _nameProperty.ContainingSymbol; - - internal override bool SynthesizesLoweredBoundBody => true; - - internal override void GenerateMethodBody(TypeCompilationState compilationState, BindingDiagnosticBag diagnostics) - { - SyntheticBoundNodeFactory F = new SyntheticBoundNodeFactory(this, CSharpSyntaxTree.Dummy.GetRoot(), compilationState, diagnostics); - F.CurrentFunction = this.OriginalDefinition; - - try - { - // return this._backingField; - F.CloseMethod(F.Return(F.Field(F.This(), _nameProperty._backingField))); - } - catch (SyntheticBoundNodeFactory.MissingPredefinedMember ex) - { - F.CloseMethod(F.ThrowNull()); - diagnostics.Add(ex.Diagnostic); - } - } - - public override bool IsStatic => false; - public override int Arity => 0; - public override bool IsExtensionMethod => false; - public override bool HidesBaseMethodsByName => false; - public override bool IsVararg => false; - public override bool ReturnsVoid => false; - public override bool IsAsync => false; - public override RefKind RefKind => RefKind.None; - public override ImmutableArray RefCustomModifiers => []; - public override TypeWithAnnotations ReturnTypeWithAnnotations => _nameProperty.TypeWithAnnotations; - public override FlowAnalysisAnnotations ReturnTypeFlowAnalysisAnnotations => FlowAnalysisAnnotations.None; - public override ImmutableHashSet ReturnNotNullIfParameterNotNull => []; - public override ImmutableArray TypeArgumentsWithAnnotations => []; - public override ImmutableArray TypeParameters => []; - public override ImmutableArray Parameters => []; - public override ImmutableArray ExplicitInterfaceImplementations => []; - public override ImmutableArray Locations => []; - public override Accessibility DeclaredAccessibility => _nameProperty.DeclaredAccessibility; - public override bool IsVirtual => false; - public override bool IsOverride => false; - public override bool IsAbstract => false; - public override bool IsSealed => false; - public override bool IsExtern => false; - protected override bool HasSetsRequiredMembersImpl => false; - internal override MethodImplAttributes ImplementationAttributes => default; - internal override bool HasDeclarativeSecurity => false; - internal override MarshalPseudoCustomAttributeData? ReturnValueMarshallingInformation => null; - internal override bool RequiresSecurityObject => false; - internal override CallingConvention CallingConvention => CallingConvention.HasThis; - internal override bool GenerateDebugInfo => false; - - public override DllImportData? GetDllImportData() => null; - internal override ImmutableArray GetAppliedConditionalSymbols() => []; - internal override IEnumerable? GetSecurityInformation() => null; - internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => false; - internal override bool IsMetadataVirtual(IsMetadataVirtualOption option = IsMetadataVirtualOption.None) => false; - } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedMemorySafetyRulesAttributeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedMemorySafetyRulesAttributeSymbol.cs new file mode 100644 index 000000000000..2182ba9780b6 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEmbeddedMemorySafetyRulesAttributeSymbol.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.CSharp.Symbols; + +/// +/// +/// namespace System.Runtime.CompilerServices +/// { +/// [CompilerGenerated, Microsoft.CodeAnalysis.Embedded] +/// [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] +/// public sealed class MemorySafetyRulesAttribute : Attribute +/// { +/// public int Version { get; } +/// public MemorySafetyRulesAttribute(int version) { Version = version; } +/// } +/// } +/// +/// +internal sealed class SynthesizedEmbeddedMemorySafetyRulesAttributeSymbol : SynthesizedEmbeddedAttributeSymbolBase +{ + private readonly ImmutableArray _fields; + private readonly ImmutableArray _properties; + private readonly ImmutableArray _constructors; + + public SynthesizedEmbeddedMemorySafetyRulesAttributeSymbol( + string name, + NamespaceSymbol containingNamespace, + ModuleSymbol containingModule, + NamedTypeSymbol systemAttributeType, + TypeSymbol int32Type) + : base(name, containingNamespace, containingModule, baseType: systemAttributeType) + { + const string PropertyName = "Version"; + + var field = new SynthesizedFieldSymbol( + containingType: this, + type: int32Type, + name: GeneratedNames.MakeBackingFieldName(PropertyName), + accessibility: DeclarationModifiers.Private, + isReadOnly: true, + isStatic: false); + + _fields = [field]; + + _properties = + [ + new SynthesizedPropertySymbol(PropertyName, field), + ]; + + _constructors = + [ + new SynthesizedEmbeddedAttributeConstructorWithBodySymbol( + containingType: this, + getParameters: m => [SynthesizedParameterSymbol.Create( + container: m, + type: TypeWithAnnotations.Create(int32Type), + ordinal: 0, + refKind: RefKind.None, + name: "version")], + getConstructorBody: GenerateConstructorBody), + ]; + } + + internal override IEnumerable GetFieldsToEmit() => _fields; + + public override ImmutableArray Constructors => _constructors; + + internal override AttributeUsageInfo GetAttributeUsageInfo() + { + return new AttributeUsageInfo(AttributeTargets.Module, allowMultiple: false, inherited: false); + } + + private void GenerateConstructorBody(SyntheticBoundNodeFactory factory, ArrayBuilder statements, ImmutableArray parameters) + { + Debug.Assert(_fields.Length == 1); + Debug.Assert(parameters.Length == 1); + statements.Add( + factory.ExpressionStatement( + factory.AssignmentExpression( + factory.Field(factory.This(), _fields[0]), + factory.Parameter(parameters[0])))); + } + + private ArrayBuilder GetMemberBuilder(Func selector) + { + var builder = ArrayBuilder.GetInstance( + _fields.Length + _properties.Length * 2 + _constructors.Length); + + builder.AddRange(_fields, selector); + + builder.AddRange(_properties, selector); + + foreach (var property in _properties) + { + Debug.Assert(property.GetMethod is not null); + Debug.Assert(property.SetMethod is null); + builder.Add(selector(property.GetMethod)); + } + + builder.AddRange(_constructors, selector); + + return builder; + } + + public override ImmutableArray GetMembers() + { + return GetMemberBuilder(static s => s).ToImmutableAndFree(); + } + + public override ImmutableArray GetMembers(string name) + { + var builder = GetMemberBuilder(static s => s); + builder.RemoveAll(static (s, name) => s.Name != name, name); + return builder.ToImmutableAndFree(); + } + + public override IEnumerable MemberNames + { + get + { + return GetMemberBuilder(static s => s.Name).ToImmutableAndFree(); + } + } +} diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEntryPointSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEntryPointSymbol.cs index 2bb7c9fac1be..a049f61eafe1 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEntryPointSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedEntryPointSymbol.cs @@ -148,6 +148,8 @@ public override bool ReturnsVoid public sealed override FlowAnalysisAnnotations FlowAnalysisAnnotations => FlowAnalysisAnnotations.None; + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => ThreeState.Unknown; + public override MethodKind MethodKind { get { return MethodKind.Ordinary; } @@ -316,6 +318,8 @@ private static BoundCall CreateParameterlessCall(CSharpSyntaxNode syntax, BoundE internal sealed override bool UseUpdatedEscapeRules => ContainingModule.UseUpdatedEscapeRules; + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal sealed override bool HasAsyncMethodBuilderAttribute(out TypeSymbol builderArgument) { builderArgument = null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedGlobalMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedGlobalMethodSymbol.cs index 565572822f98..de6cd25b135a 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedGlobalMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedGlobalMethodSymbol.cs @@ -218,6 +218,8 @@ public sealed override FlowAnalysisAnnotations FlowAnalysisAnnotations get { return FlowAnalysisAnnotations.None; } } + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => ThreeState.Unknown; + public override ImmutableArray RefCustomModifiers { get { return ImmutableArray.Empty; } @@ -358,6 +360,8 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l internal sealed override bool UseUpdatedEscapeRules => ContainingModule.UseUpdatedEscapeRules; + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal sealed override bool HasAsyncMethodBuilderAttribute(out TypeSymbol builderArgument) { builderArgument = null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedHotReloadExceptionSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedHotReloadExceptionSymbol.cs index 779124b11986..c16df2839318 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedHotReloadExceptionSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedHotReloadExceptionSymbol.cs @@ -98,7 +98,7 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r public override bool IsImplicitlyDeclared => true; internal override ManagedKind GetManagedKind(ref CompoundUseSiteInfo useSiteInfo) => ManagedKind.Managed; public override NamedTypeSymbol ConstructedFrom => this; - public override bool MightContainExtensionMethods => false; + public override bool MightContainExtensions => false; internal override bool HasDeclaredRequiredMembers => false; internal override bool IsClosed => false; public override Accessibility DeclaredAccessibility => Accessibility.Internal; @@ -122,6 +122,7 @@ internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, r internal sealed override FileIdentifier? AssociatedFileIdentifier => null; internal override bool HasCodeAnalysisEmbeddedAttribute => true; internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; internal override bool IsInterpolatedStringHandlerType => false; internal sealed override ParameterSymbol? ExtensionParameter => null; internal override bool HasSpecialName => false; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInlineArrayTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInlineArrayTypeSymbol.cs index 690d45fec9bd..b563dd5c9488 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInlineArrayTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInlineArrayTypeSymbol.cs @@ -49,7 +49,7 @@ internal SynthesizedInlineArrayTypeSymbol(SourceModuleSymbol containingModule, s public override NamedTypeSymbol ConstructedFrom => this; - public override bool MightContainExtensionMethods => false; + public override bool MightContainExtensions => false; public override string Name { get; } @@ -102,6 +102,8 @@ internal SynthesizedInlineArrayTypeSymbol(SourceModuleSymbol containingModule, s internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal override bool GetGuidString(out string? guidString) { guidString = null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInstanceConstructor.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInstanceConstructor.cs index 1bc217761bb5..2d8ff02fc2bc 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInstanceConstructor.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedInstanceConstructor.cs @@ -141,7 +141,7 @@ internal override LexicalSortKey GetLexicalSortKey() return LexicalSortKey.SynthesizedCtor; } - public sealed override ImmutableArray Locations + public override ImmutableArray Locations { get { return ContainingType.Locations; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs index 6864729af603..d79ac70c608f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedIntrinsicOperatorSymbol.cs @@ -236,6 +236,8 @@ public override TypeWithAnnotations ReturnTypeWithAnnotations public override FlowAnalysisAnnotations FlowAnalysisAnnotations => FlowAnalysisAnnotations.None; + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => ThreeState.Unknown; + public override ImmutableArray TypeArgumentsWithAnnotations { get @@ -423,6 +425,8 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l internal sealed override bool UseUpdatedEscapeRules => false; + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + public override bool Equals(Symbol obj, TypeCompareKind compareKind) { if (obj == (object)this) diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedMethodSymbol.cs index 6d68032ba9f4..aa192a3aee47 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedMethodSymbol.cs @@ -91,12 +91,16 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l public sealed override FlowAnalysisAnnotations FlowAnalysisAnnotations => FlowAnalysisAnnotations.None; + internal override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => ThreeState.Unknown; + internal override bool IsNullableAnalysisEnabled() => false; internal sealed override bool HasUnscopedRefAttribute => false; internal sealed override bool UseUpdatedEscapeRules => ContainingModule.UseUpdatedEscapeRules; + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal sealed override bool HasAsyncMethodBuilderAttribute(out TypeSymbol builderArgument) { builderArgument = null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedPrivateImplementationDetailsType.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedPrivateImplementationDetailsType.cs index 82395d3ef448..32d30ee3e69f 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedPrivateImplementationDetailsType.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedPrivateImplementationDetailsType.cs @@ -42,7 +42,7 @@ public SynthesizedPrivateImplementationDetailsType(PrivateImplementationDetails public override NamedTypeSymbol ConstructedFrom => this; - public override bool MightContainExtensionMethods => false; + public override bool MightContainExtensions => false; public override string Name => _privateImplementationDetails.Name; @@ -91,6 +91,8 @@ public SynthesizedPrivateImplementationDetailsType(PrivateImplementationDetails internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal override bool IsInterpolatedStringHandlerType => false; internal sealed override ParameterSymbol? ExtensionParameter => null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedPropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedPropertySymbol.cs new file mode 100644 index 000000000000..5e3ad1cb0ae3 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedPropertySymbol.cs @@ -0,0 +1,119 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using Microsoft.Cci; + +namespace Microsoft.CodeAnalysis.CSharp.Symbols; + +internal sealed class SynthesizedPropertySymbol : PropertySymbol +{ + private readonly string _name; + private readonly SynthesizedFieldSymbol _backingField; + + public SynthesizedPropertySymbol(string name, SynthesizedFieldSymbol backingField) + { + _name = name; + _backingField = backingField; + GetMethod = new GetAccessorMethodSymbol(this); + } + + public override string Name => _name; + public override TypeWithAnnotations TypeWithAnnotations => _backingField.TypeWithAnnotations; + public override RefKind RefKind => RefKind.None; + public override ImmutableArray RefCustomModifiers => []; + public override MethodSymbol GetMethod { get; } + public override MethodSymbol? SetMethod => null; + public override Symbol ContainingSymbol => _backingField.ContainingSymbol; + public override Accessibility DeclaredAccessibility => Accessibility.Public; + + public override ImmutableArray Locations => []; + public override ImmutableArray DeclaringSyntaxReferences => []; + public override ImmutableArray ExplicitInterfaceImplementations => []; + public override ImmutableArray Parameters => []; + public override bool IsIndexer => false; + public override bool IsStatic => false; + public override bool IsVirtual => false; + public override bool IsOverride => false; + public override bool IsAbstract => false; + public override bool IsSealed => false; + public override bool IsExtern => false; + internal override bool IsRequired => false; + internal override bool HasSpecialName => false; + internal override CallingConvention CallingConvention => CallingConvention.HasThis; + internal override bool MustCallMethodsDirectly => false; + internal override bool HasUnscopedRefAttribute => false; + internal override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal override ObsoleteAttributeData? ObsoleteAttributeData => null; + internal override int TryGetOverloadResolutionPriority() => 0; + + private sealed partial class GetAccessorMethodSymbol(SynthesizedPropertySymbol property) : SynthesizedMethodSymbol + { + private readonly SynthesizedPropertySymbol _property = property; + + public override string Name => $"get_{_property.Name}"; + internal override bool HasSpecialName => true; + public override MethodKind MethodKind => MethodKind.PropertyGet; + public override Symbol AssociatedSymbol => _property; + public override Symbol ContainingSymbol => _property.ContainingSymbol; + + internal override bool SynthesizesLoweredBoundBody => true; + + internal override void GenerateMethodBody(TypeCompilationState compilationState, BindingDiagnosticBag diagnostics) + { + SyntheticBoundNodeFactory F = new SyntheticBoundNodeFactory(this, CSharpSyntaxTree.Dummy.GetRoot(), compilationState, diagnostics); + F.CurrentFunction = this.OriginalDefinition; + + try + { + // return this._backingField; + F.CloseMethod(F.Return(F.Field(F.This(), _property._backingField))); + } + catch (SyntheticBoundNodeFactory.MissingPredefinedMember ex) + { + F.CloseMethod(F.ThrowNull()); + diagnostics.Add(ex.Diagnostic); + } + } + + public override bool IsStatic => false; + public override int Arity => 0; + public override bool IsExtensionMethod => false; + public override bool HidesBaseMethodsByName => false; + public override bool IsVararg => false; + public override bool ReturnsVoid => false; + public override bool IsAsync => false; + public override RefKind RefKind => RefKind.None; + public override ImmutableArray RefCustomModifiers => []; + public override TypeWithAnnotations ReturnTypeWithAnnotations => _property.TypeWithAnnotations; + public override FlowAnalysisAnnotations ReturnTypeFlowAnalysisAnnotations => FlowAnalysisAnnotations.None; + public override ImmutableHashSet ReturnNotNullIfParameterNotNull => []; + public override ImmutableArray TypeArgumentsWithAnnotations => []; + public override ImmutableArray TypeParameters => []; + public override ImmutableArray Parameters => []; + public override ImmutableArray ExplicitInterfaceImplementations => []; + public override ImmutableArray Locations => []; + public override Accessibility DeclaredAccessibility => _property.DeclaredAccessibility; + public override bool IsVirtual => false; + public override bool IsOverride => false; + public override bool IsAbstract => false; + public override bool IsSealed => false; + public override bool IsExtern => false; + protected override bool HasSetsRequiredMembersImpl => false; + internal override MethodImplAttributes ImplementationAttributes => default; + internal override bool HasDeclarativeSecurity => false; + internal override MarshalPseudoCustomAttributeData? ReturnValueMarshallingInformation => null; + internal override bool RequiresSecurityObject => false; + internal override CallingConvention CallingConvention => CallingConvention.HasThis; + internal override bool GenerateDebugInfo => false; + + public override DllImportData? GetDllImportData() => null; + internal override ImmutableArray GetAppliedConditionalSymbols() => []; + internal override IEnumerable? GetSecurityInformation() => null; + internal override bool IsMetadataNewSlot(bool ignoreInterfaceImplementationChanges = false) => false; + internal override bool IsMetadataVirtual(IsMetadataVirtualOption option = IsMetadataVirtualOption.None) => false; + } +} diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedStaticConstructor.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedStaticConstructor.cs index 9853f23e7f02..441b94a1100d 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedStaticConstructor.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedStaticConstructor.cs @@ -146,6 +146,8 @@ public override TypeWithAnnotations ReturnTypeWithAnnotations public override FlowAnalysisAnnotations FlowAnalysisAnnotations => FlowAnalysisAnnotations.None; + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => ThreeState.Unknown; + public override ImmutableArray RefCustomModifiers { get @@ -437,6 +439,8 @@ private bool CalculateShouldEmit(ImmutableArray boundInitializ internal sealed override bool UseUpdatedEscapeRules => false; + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal sealed override bool HasAsyncMethodBuilderAttribute(out TypeSymbol? builderArgument) { builderArgument = null; diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedUnionCtor.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedUnionCtor.cs new file mode 100644 index 000000000000..271abffc5802 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedUnionCtor.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Emit; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + internal sealed class SynthesizedUnionCtor : SynthesizedInstanceConstructor + { + private readonly int _memberOffset; + + public SynthesizedUnionCtor( + SourceMemberContainerTypeSymbol containingType, + Location location, + TypeWithAnnotations parameterType, + int memberOffset) + : base(containingType) + { + _memberOffset = memberOffset; + Parameters = [SynthesizedParameterSymbol.Create(this, parameterType, ordinal: 0, RefKind.None, ParameterSymbol.ValueParameterName)]; + Locations = [location]; + } + + public override ImmutableArray Parameters { get; } + + public override ImmutableArray Locations { get; } + + public override Accessibility DeclaredAccessibility => Accessibility.Public; + + internal override LexicalSortKey GetLexicalSortKey() => LexicalSortKey.GetSynthesizedMemberKey(_memberOffset); + + internal override void GenerateMethodBodyStatements(SyntheticBoundNodeFactory F, ArrayBuilder statements, BindingDiagnosticBag diagnostics) + { + // Write an assignment to Value property + // { + // this._ValueBackingField = parameter + // } + var valueProperty = ContainingType.GetMembers(WellKnownMemberNames.ValuePropertyName).OfType().Single(); + Debug.Assert(valueProperty.DeclaredBackingField is not null); + BoundParameter parameter = F.Parameter(Parameters[0]); + + var useSiteInfo = CompoundUseSiteInfo.Discarded; + Conversion c = F.Compilation.Conversions.ClassifyImplicitConversionFromType(parameter.Type, valueProperty.Type, ref useSiteInfo); + + if (IsValidParameterTypeConversion(c)) + { + statements.Add(F.Assignment(F.Field(F.This(), valueProperty.DeclaredBackingField), F.Convert(valueProperty.Type, parameter, c, explicitCastInCode: false))); + } + else + { + statements.Add(new BoundNoOpStatement(F.Syntax, NoOpStatementFlavor.Default, hasErrors: true)); + } + + // Add a sequence point at the end of the constructor, so that a breakpoint placed on the case type + // can be hit whenever a new instance of the union for that case type is created. + Debug.Assert(F.Syntax is TypeDeclarationSyntax); + statements.Add(new BoundSequencePointWithSpan(F.Syntax, statementOpt: null, Locations[0].SourceSpan)); // https://github.com/dotnet/roslyn/issues/82636: Add test coverage and verify debugging experience. + } + + public static bool IsValidParameterTypeConversion(Conversion c) + { + return c.Exists && c.IsImplicit && (c.IsIdentity || c.IsReference || c.IsBoxing); + } + + internal override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, ref ArrayBuilder attributes) + { + base.AddSynthesizedAttributes(moduleBuilder, ref attributes); + Debug.Assert(IsImplicitlyDeclared); + var compilation = this.DeclaringCompilation; + AddSynthesizedAttribute(ref attributes, compilation.TrySynthesizeAttribute(WellKnownMember.System_Runtime_CompilerServices_CompilerGeneratedAttribute__ctor)); + Debug.Assert(WellKnownMembers.IsSynthesizedAttributeOptional(WellKnownMember.System_Runtime_CompilerServices_CompilerGeneratedAttribute__ctor)); + } + + internal override void AfterAddingTypeMembersChecks(ConversionsBase conversions, BindingDiagnosticBag diagnostics) + { + base.AfterAddingTypeMembersChecks(conversions, diagnostics); + + this.Parameters[0].Type.CheckAllConstraints(DeclaringCompilation, conversions, GetFirstLocation(), diagnostics); + } + } +} + diff --git a/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedUnionValuePropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedUnionValuePropertySymbol.cs new file mode 100644 index 000000000000..8a6ca53d3fce --- /dev/null +++ b/src/Compilers/CSharp/Portable/Symbols/Synthesized/SynthesizedUnionValuePropertySymbol.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.CodeAnalysis.CSharp.Symbols +{ + internal sealed class SynthesizedUnionValuePropertySymbol : SourcePropertySymbolBase + { + public SynthesizedUnionValuePropertySymbol( + SourceMemberContainerTypeSymbol containingType, + TypeDeclarationSyntax syntax, + BindingDiagnosticBag diagnostics) + : base( + containingType, + syntax: syntax, + hasGetAccessor: true, + hasSetAccessor: false, + isExplicitInterfaceImplementation: false, + explicitInterfaceType: null, + aliasQualifierOpt: null, + modifiers: DeclarationModifiers.Public, + hasInitializer: false, + hasExplicitAccessMod: false, + hasAutoPropertyGet: true, + hasAutoPropertySet: false, + isExpressionBodied: false, + accessorsHaveImplementation: true, + getterUsesFieldKeyword: false, + setterUsesFieldKeyword: false, + RefKind.None, + WellKnownMemberNames.ValuePropertyName, + indexerNameAttributeLists: new SyntaxList(), + syntax.Location, + diagnostics) + { + } + + public override bool IsImplicitlyDeclared => true; + + protected override SourcePropertySymbolBase? BoundAttributesSource => null; + + public override IAttributeTargetSymbol AttributesOwner => this; + + protected override Location TypeLocation + => ((TypeDeclarationSyntax)CSharpSyntaxNode).Identifier.GetLocation(); + + public override OneOrMany> GetAttributeDeclarations() + => OneOrMany>.Empty; + + protected override SourcePropertyAccessorSymbol CreateGetAccessorSymbol(bool isAutoPropertyAccessor, BindingDiagnosticBag diagnostics) + { + Debug.Assert(isAutoPropertyAccessor); + return SourcePropertyAccessorSymbol.CreateAccessorSymbol( + ContainingType, + this, + _modifiers, + TypeLocation, + CSharpSyntaxNode, + diagnostics); + } + + protected override SourcePropertyAccessorSymbol CreateSetAccessorSymbol(bool isAutoPropertyAccessor, BindingDiagnosticBag diagnostics) + { + throw ExceptionUtilities.Unreachable(); + } + + protected override (TypeWithAnnotations Type, ImmutableArray Parameters) MakeParametersAndBindType(BindingDiagnosticBag diagnostics) + { + return (TypeWithAnnotations.Create(Binder.GetSpecialType(DeclaringCompilation, SpecialType.System_Object, TypeLocation, diagnostics), nullableAnnotation: NullableAnnotation.Annotated), + ImmutableArray.Empty); + } + + internal override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + } +} diff --git a/src/Compilers/CSharp/Portable/Symbols/TypeParameterSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/TypeParameterSymbol.cs index 30fe4bc28260..f6fae76b6e54 100644 --- a/src/Compilers/CSharp/Portable/Symbols/TypeParameterSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/TypeParameterSymbol.cs @@ -725,5 +725,28 @@ internal sealed override bool HasInlineArrayAttribute(out int length) length = 0; return false; } + + internal byte GetSynthesizedNullableAttributeValue() + { + if (this.HasReferenceTypeConstraint) + { + switch (this.ReferenceTypeConstraintIsNullable) + { + case true: + return NullableAnnotationExtensions.AnnotatedAttributeValue; + case false: + return NullableAnnotationExtensions.NotAnnotatedAttributeValue; + } + } + else if (this.HasNotNullConstraint) + { + return NullableAnnotationExtensions.NotAnnotatedAttributeValue; + } + else if (!this.HasValueTypeConstraint && this.ConstraintTypesNoUseSiteDiagnostics.IsEmpty && this.IsNotNullable == false) + { + return NullableAnnotationExtensions.AnnotatedAttributeValue; + } + return NullableAnnotationExtensions.ObliviousAttributeValue; + } } } diff --git a/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs index fd5d1f58b13c..f823e62af150 100644 --- a/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/TypeSymbol.cs @@ -660,6 +660,8 @@ internal TypeSymbol SetUnknownNullabilityForReferenceTypes() /// public abstract bool IsReadOnly { get; } + internal sealed override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + public string ToDisplayString(CodeAnalysis.NullableFlowState topLevelNullability, SymbolDisplayFormat format = null) { return SymbolDisplay.ToDisplayString((ITypeSymbol)ISymbol, topLevelNullability, format); @@ -1794,6 +1796,14 @@ internal static void CheckModifierMismatchOnImplementingMember(TypeSymbol implem } }, (implementingType, isExplicit)); + + SourceMemberContainerTypeSymbol.CheckCallerUnsafeMismatch( + implementedEvent, + implementingEvent, + isExplicit ? ErrorCode.ERR_CallerUnsafeExplicitlyImplementingSafe : ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, + (implementedEvent, implementingType, implementingEvent), + static arg => GetImplicitImplementationDiagnosticLocation(arg.implementedEvent, arg.implementingType, arg.implementingEvent), + diagnostics); } else { @@ -1880,6 +1890,7 @@ static void checkMethodOverride( allowVariance: true, invokedAsExtensionMethod: false); } + SourceMemberContainerTypeSymbol.CheckRefReadonlyInMismatch( implementedMethod, implementingMethod, diagnostics, static (diagnostics, implementedMethod, implementingMethod, implementingParameter, _, arg) => @@ -1892,6 +1903,14 @@ static void checkMethodOverride( implementingType, invokedAsExtensionMethod: false); + SourceMemberContainerTypeSymbol.CheckCallerUnsafeMismatch( + implementedMethod, + implementingMethod, + isExplicit ? ErrorCode.ERR_CallerUnsafeExplicitlyImplementingSafe : ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, + (implementedMethod, implementingType, implementingMethod), + static arg => GetImplicitImplementationDiagnosticLocation(arg.implementedMethod, arg.implementingType, arg.implementingMethod), + diagnostics); + if (implementingMethod.HasUnscopedRefAttributeOnMethodOrProperty()) { if (implementedMethod.HasUnscopedRefAttributeOnMethodOrProperty()) diff --git a/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs b/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs index 8d4a0e025fe8..69abcd6f11c7 100644 --- a/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs +++ b/src/Compilers/CSharp/Portable/Symbols/TypeSymbolExtensions.cs @@ -182,6 +182,23 @@ public static TypeSymbol StrippedType(this TypeSymbol type) return type.IsNullableType() ? type.GetNullableUnderlyingType() : type; } + extension(TypeSymbol patternInputType) + { + public bool IsSubjectForUnionMatching => patternInputType.StrippedType() is NamedTypeSymbol { IsUnionType: true }; + + public bool IsUnionMatchingInputType([NotNullWhen(true)] out NamedTypeSymbol? unionType) + { + if (patternInputType.StrippedType() is NamedTypeSymbol { IsUnionType: true } named) + { + unionType = named; + return true; + } + + unionType = null; + return false; + } + } + public static TypeSymbol EnumUnderlyingTypeOrSelf(this TypeSymbol type) { return type.GetEnumUnderlyingType() ?? type; diff --git a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedEventSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedEventSymbol.cs index 43e0bb8dde60..a7ba977c10d7 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedEventSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedEventSymbol.cs @@ -165,6 +165,8 @@ internal override bool HasRuntimeSpecialName } } + internal sealed override CallerUnsafeMode CallerUnsafeMode => _underlyingEvent.CallerUnsafeMode; + // If we need to un-seal this method, we should make it abstract. internal sealed override void AddSynthesizedAttributes(PEModuleBuilder moduleBuilder, ref ArrayBuilder attributes) => throw ExceptionUtilities.Unreachable(); diff --git a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs index 6e1aa0c6634e..3409dd6f1574 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedMethodSymbol.cs @@ -140,14 +140,6 @@ public override bool IsVirtual } } - public override bool IsAsync - { - get - { - return UnderlyingMethod.IsAsync; - } - } - public override bool IsOverride { get @@ -188,6 +180,8 @@ public override bool IsImplicitlyDeclared } } + internal override CallerUnsafeMode CallerUnsafeMode => UnderlyingMethod.CallerUnsafeMode; + internal override bool IsMetadataVirtual(IsMetadataVirtualOption option = IsMetadataVirtualOption.None) { return UnderlyingMethod.IsMetadataVirtual(option); diff --git a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedNamedTypeSymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedNamedTypeSymbol.cs index cad54040b692..ddefee170166 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedNamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedNamedTypeSymbol.cs @@ -57,11 +57,11 @@ public override int Arity } } - public override bool MightContainExtensionMethods + public override bool MightContainExtensions { get { - return _underlyingType.MightContainExtensionMethods; + return _underlyingType.MightContainExtensions; } } diff --git a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedPropertySymbol.cs b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedPropertySymbol.cs index 61d74b428d52..12c5ceb887c4 100644 --- a/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedPropertySymbol.cs +++ b/src/Compilers/CSharp/Portable/Symbols/Wrapped/WrappedPropertySymbol.cs @@ -167,6 +167,8 @@ public override bool IsExtern internal sealed override bool HasUnscopedRefAttribute => _underlyingProperty.HasUnscopedRefAttribute; + internal sealed override CallerUnsafeMode CallerUnsafeMode => _underlyingProperty.CallerUnsafeMode; + internal override ObsoleteAttributeData ObsoleteAttributeData { get diff --git a/src/Compilers/CSharp/Portable/Syntax/Syntax.xml b/src/Compilers/CSharp/Portable/Syntax/Syntax.xml index aada4dc19205..66fac99abfc2 100644 --- a/src/Compilers/CSharp/Portable/Syntax/Syntax.xml +++ b/src/Compilers/CSharp/Portable/Syntax/Syntax.xml @@ -1839,7 +1839,7 @@ - + @@ -3398,7 +3398,7 @@ - Gets the type keyword token ("class", "struct", "interface", "record", "extension"). + Gets the type keyword token ("class", "struct", "interface", "record", "extension", "union"). @@ -3450,13 +3450,15 @@ Struct type declaration syntax. + - Gets the struct keyword token. + Gets the struct or union keyword token. + diff --git a/src/Compilers/CSharp/Portable/Syntax/Syntax.xsd b/src/Compilers/CSharp/Portable/Syntax/Syntax.xsd index ef8ce1b6cf16..6e5257dc8e85 100644 --- a/src/Compilers/CSharp/Portable/Syntax/Syntax.xsd +++ b/src/Compilers/CSharp/Portable/Syntax/Syntax.xsd @@ -8,6 +8,7 @@ + @@ -34,6 +35,7 @@ + @@ -60,6 +62,7 @@ + @@ -83,6 +86,7 @@ + @@ -95,6 +99,7 @@ + @@ -104,6 +109,7 @@ + @@ -131,6 +137,7 @@ + @@ -170,6 +177,7 @@ + @@ -182,6 +190,7 @@ + @@ -206,6 +215,7 @@ + @@ -220,6 +230,7 @@ + @@ -227,4 +238,4 @@ - \ No newline at end of file + diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxFactory.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxFactory.cs index 0800ea7252b7..f21d1628bce5 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxFactory.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxFactory.cs @@ -1845,7 +1845,7 @@ public static ParameterListSyntax ParseParameterList(string text, int offset = 0 using (var lexer = MakeLexer(text, offset, (CSharpParseOptions?)options)) using (var parser = MakeParser(lexer)) { - var node = parser.ParseParenthesizedParameterList(forExtension: false); + var node = parser.ParseParenthesizedParameterList(forExtensionOrUnion: false); if (consumeFullText) node = parser.ConsumeUnexpectedTokens(node); return CreateRed(node, lexer.Options); } @@ -2984,5 +2984,11 @@ public static EnumDeclarationSyntax EnumDeclaration(SyntaxToken identifier) /// Creates a new EnumDeclarationSyntax instance. public static EnumDeclarationSyntax EnumDeclaration(string identifier) => SyntaxFactory.EnumDeclaration(default, default(SyntaxTokenList), SyntaxFactory.Token(SyntaxKind.EnumKeyword), SyntaxFactory.Identifier(identifier), null, SyntaxFactory.Token(SyntaxKind.OpenBraceToken), default, SyntaxFactory.Token(SyntaxKind.CloseBraceToken), default); + + /// Creates a new StructDeclarationSyntax instance. + public static StructDeclarationSyntax StructDeclaration(SyntaxList attributeLists, SyntaxTokenList modifiers, SyntaxToken keyword, SyntaxToken identifier, TypeParameterListSyntax? typeParameterList, ParameterListSyntax? parameterList, BaseListSyntax? baseList, SyntaxList constraintClauses, SyntaxToken openBraceToken, SyntaxList members, SyntaxToken closeBraceToken, SyntaxToken semicolonToken) + => StructDeclaration( + keyword.Kind() is SyntaxKind.UnionKeyword ? SyntaxKind.UnionDeclaration : SyntaxKind.StructDeclaration, + attributeLists, modifiers, keyword, identifier, typeParameterList, parameterList, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); } } diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxKind.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxKind.cs index 94ec14e0f144..78dc07d268a5 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxKind.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxKind.cs @@ -2,9 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; + namespace Microsoft.CodeAnalysis.CSharp { #pragma warning disable CA1200 // Avoid using cref tags with a prefix - The prefix is required since this file is referenced in projects that can't access syntax nodes + // When adding new experimental kinds, you will need to manually specify RSEXPERIMENTAL006, as not all projects that reference this file have RoslynExperiments available. // DO NOT CHANGE NUMBERS ASSIGNED TO EXISTING KINDS OR YOU WILL BREAK BINARY COMPATIBILITY public enum SyntaxKind : ushort { @@ -423,8 +426,11 @@ public enum SyntaxKind : ushort AllowsKeyword = 8450, /// Represents . ExtensionKeyword = 8451, + /// Represents . + [Experimental("RSEXPERIMENTAL006", UrlFormat = "https://github.com/dotnet/roslyn/issues/82567")] + UnionKeyword = 8452, /// Represents . - ClosedKeyword = 8452, + ClosedKeyword = 8453, // when adding a contextual keyword following functions must be adapted: // @@ -935,6 +941,10 @@ public enum SyntaxKind : ushort IgnoredDirectiveTrivia = 9080, + [Experimental("RSEXPERIMENTAL006", UrlFormat = "https://github.com/dotnet/roslyn/issues/82210")] WithElement = 9081, + + [Experimental("RSEXPERIMENTAL006", UrlFormat = "https://github.com/dotnet/roslyn/issues/82567")] + UnionDeclaration = 9082, } } diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs index 29df89f426f8..4847b97dd6e7 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxKindFacts.cs @@ -379,6 +379,7 @@ public static bool IsTypeDeclaration(SyntaxKind kind) { case SyntaxKind.ClassDeclaration: case SyntaxKind.StructDeclaration: + case SyntaxKind.UnionDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.DelegateDeclaration: case SyntaxKind.EnumDeclaration: @@ -861,6 +862,8 @@ public static SyntaxKind GetTypeDeclarationKind(SyntaxKind kind) return SyntaxKind.ClassDeclaration; case SyntaxKind.StructKeyword: return SyntaxKind.StructDeclaration; + case SyntaxKind.UnionKeyword: + return SyntaxKind.UnionDeclaration; case SyntaxKind.InterfaceKeyword: return SyntaxKind.InterfaceDeclaration; case SyntaxKind.RecordKeyword: @@ -1297,6 +1300,7 @@ public static bool IsContextualKeyword(SyntaxKind kind) case SyntaxKind.FileKeyword: case SyntaxKind.AllowsKeyword: case SyntaxKind.ExtensionKeyword: + case SyntaxKind.UnionKeyword: case SyntaxKind.ClosedKeyword: return true; default: @@ -1427,6 +1431,8 @@ public static SyntaxKind GetContextualKeywordKind(string text) return SyntaxKind.AllowsKeyword; case "extension": return SyntaxKind.ExtensionKeyword; + case "union": + return SyntaxKind.UnionKeyword; case "closed": return SyntaxKind.ClosedKeyword; default: @@ -1878,6 +1884,8 @@ public static string GetText(SyntaxKind kind) return "allows"; case SyntaxKind.ExtensionKeyword: return "extension"; + case SyntaxKind.UnionKeyword: + return "union"; case SyntaxKind.ClosedKeyword: return "closed"; default: diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxNodeRemover.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxNodeRemover.cs index f7c480ce7a00..4cbed3fea94c 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxNodeRemover.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxNodeRemover.cs @@ -431,7 +431,7 @@ private void AddDirectives(SyntaxNode node, TextSpan span) _directivesToKeep.Clear(); } - var directivesInSpan = node.DescendantTrivia(span, n => n.ContainsDirectives, descendIntoTrivia: true) + var directivesInSpan = node.DescendantTrivia(span, descendIntoChildrenGreen: static n => n.ContainsDirectives, descendIntoChildrenRed: null, descendIntoTrivia: true) .Where(tr => tr.IsDirective) .Select(tr => (DirectiveTriviaSyntax)tr.GetStructure()!); diff --git a/src/Compilers/CSharp/Portable/Syntax/TypeDeclarationSyntax.cs b/src/Compilers/CSharp/Portable/Syntax/TypeDeclarationSyntax.cs index 58a8383b2bde..f0cfb91c115b 100644 --- a/src/Compilers/CSharp/Portable/Syntax/TypeDeclarationSyntax.cs +++ b/src/Compilers/CSharp/Portable/Syntax/TypeDeclarationSyntax.cs @@ -57,6 +57,8 @@ internal static SyntaxKind GetTypeDeclarationKeywordKind(DeclarationKind kind) return SyntaxKind.ClassKeyword; case DeclarationKind.Struct: return SyntaxKind.StructKeyword; + case DeclarationKind.Union: + return SyntaxKind.UnionKeyword; case DeclarationKind.Interface: return SyntaxKind.InterfaceKeyword; default: @@ -72,6 +74,8 @@ private static SyntaxKind GetTypeDeclarationKeywordKind(SyntaxKind kind) return SyntaxKind.ClassKeyword; case SyntaxKind.StructDeclaration: return SyntaxKind.StructKeyword; + case SyntaxKind.UnionDeclaration: + return SyntaxKind.UnionKeyword; case SyntaxKind.InterfaceDeclaration: return SyntaxKind.InterfaceKeyword; case SyntaxKind.RecordDeclaration: @@ -123,7 +127,9 @@ public static TypeDeclarationSyntax TypeDeclaration( case SyntaxKind.ClassDeclaration: return SyntaxFactory.ClassDeclaration(attributes, modifiers, keyword, identifier, typeParameterList, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); case SyntaxKind.StructDeclaration: - return SyntaxFactory.StructDeclaration(attributes, modifiers, keyword, identifier, typeParameterList, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); + return SyntaxFactory.StructDeclaration(SyntaxKind.StructDeclaration, attributes, modifiers, keyword, identifier, typeParameterList, parameterList: null, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); + case SyntaxKind.UnionDeclaration: + return SyntaxFactory.StructDeclaration(SyntaxKind.UnionDeclaration, attributes, modifiers, keyword, identifier, typeParameterList, parameterList: null, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); case SyntaxKind.InterfaceDeclaration: return SyntaxFactory.InterfaceDeclaration(attributes, modifiers, keyword, identifier, typeParameterList, baseList, constraintClauses, openBraceToken, members, closeBraceToken, semicolonToken); case SyntaxKind.RecordDeclaration: diff --git a/src/Compilers/CSharp/Portable/Utilities/IValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/IConstantValueSetFactory.cs similarity index 76% rename from src/Compilers/CSharp/Portable/Utilities/IValueSetFactory.cs rename to src/Compilers/CSharp/Portable/Utilities/IConstantValueSetFactory.cs index 769b43a0b7a8..51beadcbbabe 100644 --- a/src/Compilers/CSharp/Portable/Utilities/IValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/IConstantValueSetFactory.cs @@ -7,15 +7,15 @@ namespace Microsoft.CodeAnalysis.CSharp { /// - /// A value set factory, which can be used to create a value set instance. A given instance of + /// A value set factory, which can be used to create a value set instance. A given instance of /// supports only one type for the value sets it can produce. /// - internal interface IValueSetFactory + internal interface IConstantValueSetFactory { /// /// Returns a value set that includes any values that satisfy the given relation when compared to the given value. /// - IValueSet Related(BinaryOperatorKind relation, ConstantValue value); + IConstantValueSet Related(BinaryOperatorKind relation, ConstantValue value); /// /// Returns true iff the values are related according to the given relation. @@ -25,7 +25,7 @@ internal interface IValueSetFactory /// /// Produce a random value set with the given expected size for testing. /// - IValueSet Random(int expectedSize, Random random); + IConstantValueSet Random(int expectedSize, Random random); /// /// Produce a random value for testing. @@ -35,23 +35,23 @@ internal interface IValueSetFactory /// /// The set containing all values of the type. /// - IValueSet AllValues { get; } + IConstantValueSet AllValues { get; } /// /// The empty set of values. /// - IValueSet NoValues { get; } + IConstantValueSet NoValues { get; } } /// /// A value set factory, which can be used to create a value set instance. Like but strongly /// typed to . /// - internal interface IValueSetFactory : IValueSetFactory + internal interface IConstantValueSetFactory : IConstantValueSetFactory { /// /// Returns a value set that includes any values that satisfy the given relation when compared to the given value. /// - IValueSet Related(BinaryOperatorKind relation, T value); + IConstantValueSet Related(BinaryOperatorKind relation, T value); } } diff --git a/src/Compilers/CSharp/Portable/Utilities/ITypeUnionValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ITypeUnionValueSetFactory.cs new file mode 100644 index 000000000000..f15db9864e90 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Utilities/ITypeUnionValueSetFactory.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.CSharp.Symbols; + +namespace Microsoft.CodeAnalysis.CSharp +{ + internal interface ITypeUnionValueSetFactory + { + TypeUnionValueSet AllValues(ConversionsBase conversions); + TypeUnionValueSet FromTypeMatch(TypeSymbol type, ConversionsBase conversions, ref CompoundUseSiteInfo useSiteInfo); + TypeUnionValueSet FromNullMatch(ConversionsBase conversions); + TypeUnionValueSet FromNonNullMatch(ConversionsBase conversions); + } +} diff --git a/src/Compilers/CSharp/Portable/Utilities/IValueSet.cs b/src/Compilers/CSharp/Portable/Utilities/IValueSet.cs index 9be904f16764..2cae3dbff372 100644 --- a/src/Compilers/CSharp/Portable/Utilities/IValueSet.cs +++ b/src/Compilers/CSharp/Portable/Utilities/IValueSet.cs @@ -16,12 +16,12 @@ namespace Microsoft.CodeAnalysis.CSharp internal interface IValueSet { /// - /// Return the intersection of this value set with another. Both must have been created with the same . + /// Return the intersection of this value set with another. Both must have been created with the same . /// IValueSet Intersect(IValueSet other); /// - /// Return this union of this value set with another. Both must have been created with the same . + /// Return this union of this value set with another. Both must have been created with the same . /// IValueSet Union(IValueSet other); @@ -29,7 +29,10 @@ internal interface IValueSet /// Return the complement of this value set. /// IValueSet Complement(); + } + internal interface IConstantValueSet : IValueSet + { /// /// Test if the value set contains any values that satisfy the given relation with the given value. Supported values for /// are for all supported types, and for numeric types we also support @@ -62,22 +65,22 @@ internal interface IValueSet /// /// An interface representing a set of values of a specific type. Like but strongly typed to . /// - internal interface IValueSet : IValueSet + internal interface IConstantValueSet : IConstantValueSet { /// - /// Return the intersection of this value set with another. Both must have been created with the same . + /// Return the intersection of this value set with another. Both must have been created with the same . /// - IValueSet Intersect(IValueSet other); + IConstantValueSet Intersect(IConstantValueSet other); /// - /// Return this union of this value set with another. Both must have been created with the same . + /// Return this union of this value set with another. Both must have been created with the same . /// - IValueSet Union(IValueSet other); + IConstantValueSet Union(IConstantValueSet other); /// /// Return the complement of this value set. /// - new IValueSet Complement(); + new IConstantValueSet Complement(); /// /// Test if the value set contains any values that satisfy the given relation with the given value. diff --git a/src/Compilers/CSharp/Portable/Utilities/TypeUnionValueSet.cs b/src/Compilers/CSharp/Portable/Utilities/TypeUnionValueSet.cs new file mode 100644 index 000000000000..4ec64c66f0cf --- /dev/null +++ b/src/Compilers/CSharp/Portable/Utilities/TypeUnionValueSet.cs @@ -0,0 +1,511 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.CSharp +{ + /// + /// The implementation of a value set for instances of types from a given set (a union), and a 'null' value. + /// + /// For the sake of simplicity, the implementation is intentionally kept not thread safe. + /// + internal sealed class TypeUnionValueSet : IValueSet + { + private readonly ConversionsBase _conversions; + + /// + /// The set of types defining a union of types, instances of which could be in the value set. + /// + private readonly ImmutableArray _typesInUnion; + + /// + /// Root of a logical tree defining values contained in this value set. + /// + /// If an instance of type cannot be an instance of any of the types in the union, + /// instances of that type are definitely not in the set. Otherwise, + /// defines if instances of a given type are in the set. + /// + /// See function. + /// If the tree evaluates to true for a given input type, instances of that type are definitely in the set. + /// If the tree evaluates to false for a given input type, instances of that type are definitely not in the set. + /// If the tree evaluates to 'null' (or an unknown result) for a given input type, instances of that type + /// might or might not be in the set, we cannot give a definitive answer. + /// + /// + private readonly Node _root; + + private bool? _lazyMightIncludeNonNull; + private bool? _lazyIncludesNull; + + private TypeUnionValueSet( + ImmutableArray typesInUnion, + Node root, + ConversionsBase conversions) + { + Debug.Assert(!typesInUnion.IsEmpty); + Debug.Assert(!typesInUnion.Any(t => t.IsNullableType())); + Debug.Assert(typesInUnion.Distinct().Length == typesInUnion.Length); + _typesInUnion = typesInUnion; + _root = root; + _conversions = conversions; + } + + internal static TypeUnionValueSet AllValues(ImmutableArray typesInUnion, ConversionsBase conversions) + { + return new TypeUnionValueSet(typesInUnion, IsTrueNode.Instance, conversions); + } + + internal static TypeUnionValueSet FromTypeMatch(ImmutableArray typesInUnion, TypeSymbol type, ConversionsBase conversions, ref CompoundUseSiteInfo useSiteInfo) + { + if (AnyTypeFromUnionMightMatch(typesInUnion, type, conversions, ref useSiteInfo)) + { + return new TypeUnionValueSet(typesInUnion, new IsTypeNode(type), conversions); + } + + // An empty set + return new TypeUnionValueSet(typesInUnion, IsFalseNode.Instance, conversions); + } + + private static bool AnyTypeFromUnionMightMatch(ImmutableArray typesInUnion, TypeSymbol type, ConversionsBase conversions, ref CompoundUseSiteInfo useSiteInfo) + { + Debug.Assert(!typesInUnion.IsEmpty); + Debug.Assert(!typesInUnion.Any(TypeSymbolExtensions.IsNullableType)); + Debug.Assert(typesInUnion.Distinct().Length == typesInUnion.Length); + + foreach (var t in typesInUnion) + { + ConstantValue? matches = DecisionDagBuilder.ExpressionOfTypeMatchesPatternTypeForLearningFromSuccessfulTypeTest(conversions, type, t, ref useSiteInfo); + if (matches == ConstantValue.False) + { + // If 'type' could never be 't' + // v is type --> !(v is t) + continue; + } + + return true; + } + + return false; + } + + internal static TypeUnionValueSet FromNullMatch(ImmutableArray typesInUnion, ConversionsBase conversions) + { + return new TypeUnionValueSet(typesInUnion, IsNullNode.Instance, conversions); + } + + internal static TypeUnionValueSet FromNonNullMatch(ImmutableArray typesInUnion, ConversionsBase conversions) + { + return new TypeUnionValueSet(typesInUnion, new NotNode(IsNullNode.Instance), conversions); + } + + public bool MightIncludeNonNull(ref CompoundUseSiteInfo useSiteInfo) + { + if (!_lazyMightIncludeNonNull.HasValue) + { + if (_root == (object)IsTrueNode.Instance) + { + _lazyMightIncludeNonNull = true; + } + else if (_root == (object)IsFalseNode.Instance) + { + _lazyMightIncludeNonNull = false; + } + else + { + _lazyMightIncludeNonNull = TryGetSampleType(_root, ref useSiteInfo) is not null; + } + } + + return _lazyMightIncludeNonNull.GetValueOrDefault(); + } + + public bool IncludesNull + { + get + { + if (!_lazyIncludesNull.HasValue) + { + if (_root == (object)IsTrueNode.Instance) + { + _lazyIncludesNull = true; + } + else if (_root == (object)IsFalseNode.Instance) + { + _lazyIncludesNull = false; + } + else + { + // Null checks do not check conversions, therefore we can pass discarded use-site info, + // and not ask consumers to pass it to us. + var discardedInfo = CompoundUseSiteInfo.Discarded; + bool? result = EvaluateNodeForInputValue(_root, null, ref discardedInfo); + Debug.Assert(result.HasValue); + _lazyIncludesNull = result.GetValueOrDefault(); + } + } + + return _lazyIncludesNull.GetValueOrDefault(); + } + } + + /// + /// Returns true only when the set is definetely empty, i.e. it does not include 'null' value and + /// definitely doesn't include an instance of a type from the union. + /// + public bool IsEmpty(ref CompoundUseSiteInfo useSiteInfo) + { + return !IncludesNull && !MightIncludeNonNull(ref useSiteInfo); + } + + /// Type symbol, or 'null' when we want to perform a check for null value. + private bool? EvaluateNodeForInputValue(Node node, TypeSymbol? inputValue, ref CompoundUseSiteInfo useSiteInfo) + { + switch (node) + { + case IsTrueNode: + return true; + case IsFalseNode: + return false; + case IsTypeNode { Type: var t2 }: + { + switch (inputValue) + { + case null: + return false; + case TypeSymbol t1: + return evaluateTypeMatch(t1, t2, ref useSiteInfo); + default: + throw ExceptionUtilities.UnexpectedValue(inputValue); + } + } + case NotNode not: + { + return !EvaluateNodeForInputValue(not.Negated, inputValue, ref useSiteInfo); + } + case IsNullNode: + { + switch (inputValue) + { + case null: + return true; + case TypeSymbol: + return false; + default: + throw ExceptionUtilities.UnexpectedValue(inputValue); + } + } + case AndNode andNode: + { + var leftResult = EvaluateNodeForInputValue(andNode.Left, inputValue, ref useSiteInfo); + var rightResult = EvaluateNodeForInputValue(andNode.Right, inputValue, ref useSiteInfo); + if (leftResult == false || rightResult == false) + return false; + if (leftResult == true && rightResult == true) + return true; + + // Propagate unknown + return null; + } + case OrNode orNode: + { + var leftResult = EvaluateNodeForInputValue(orNode.Left, inputValue, ref useSiteInfo); + var rightResult = EvaluateNodeForInputValue(orNode.Right, inputValue, ref useSiteInfo); + if (leftResult == true || rightResult == true) + return true; + if (leftResult == false && rightResult == false) + return false; + + // Propagate unknown + return null; + } + default: + throw ExceptionUtilities.UnexpectedValue(node); + } + + bool? evaluateTypeMatch(TypeSymbol t1, TypeSymbol t2, ref CompoundUseSiteInfo useSiteInfo) + { + ConstantValue? matches = DecisionDagBuilder.ExpressionOfTypeMatchesPatternTypeForLearningFromSuccessfulTypeTest(_conversions, t1, t2, ref useSiteInfo); + if (matches == ConstantValue.False) + { + // If T1 could never be T2 + // v is T1 --> !(v is T2) + return false; + } + else if (matches == ConstantValue.True) + { + // If T1: T2 + // v is T1 --> v is T2 + return true; + } + + return null; + } + } + + public TypeSymbol? SampleType(ref CompoundUseSiteInfo useSiteInfo) + { + if (IsEmpty(ref useSiteInfo)) + throw new ArgumentException(); + + if (_lazyMightIncludeNonNull != false) + { + return TryGetSampleType(_root, ref useSiteInfo); + } + + return null; + } + + private TypeSymbol? TryGetSampleType(Node root, ref CompoundUseSiteInfo useSiteInfo) + { + foreach (var t in _typesInUnion) + { + if (EvaluateNodeForInputValue(root, t, ref useSiteInfo) != false) + return t; + } + + return null; + } + + IValueSet IValueSet.Complement() + { + return Complement(); + } + + IValueSet IValueSet.Intersect(IValueSet other) + { + return Intersect((TypeUnionValueSet)other); + } + + IValueSet IValueSet.Union(IValueSet other) + { + return Union((TypeUnionValueSet)other); + } + + public TypeUnionValueSet Complement() + { + if (_root == (object)IsTrueNode.Instance) + { + return new TypeUnionValueSet(_typesInUnion, IsFalseNode.Instance, _conversions); + } + + if (_root == (object)IsFalseNode.Instance) + { + return new TypeUnionValueSet(_typesInUnion, IsTrueNode.Instance, _conversions); + } + + if (_root is not NotNode { Negated: var negated }) + { + negated = new NotNode(_root); + } + + return new TypeUnionValueSet(_typesInUnion, negated, _conversions); + } + + public TypeUnionValueSet Intersect(TypeUnionValueSet other) + { + Debug.Assert(_typesInUnion.SequenceEqual(other._typesInUnion)); + + if (_root == (object)IsFalseNode.Instance) + { + return this; + } + + if (other._root == (object)IsFalseNode.Instance) + { + return other; + } + + if (_root == (object)IsTrueNode.Instance) + { + return other; + } + + if (other._root == (object)IsTrueNode.Instance) + { + return this; + } + + return new TypeUnionValueSet(_typesInUnion, new AndNode(_root, other._root), _conversions); + } + + public TypeUnionValueSet Union(TypeUnionValueSet other) + { + Debug.Assert(_typesInUnion.SequenceEqual(other._typesInUnion)); + + if (_root == (object)IsFalseNode.Instance) + { + return other; + } + + if (other._root == (object)IsFalseNode.Instance) + { + return this; + } + + if (_root == (object)IsTrueNode.Instance) + { + return this; + } + + if (other._root == (object)IsTrueNode.Instance) + { + return other; + } + + return new TypeUnionValueSet(_typesInUnion, new OrNode(_root, other._root), _conversions); + } + + public bool TypeMatchesAllValuesIfAny(TypeSymbol type, ref CompoundUseSiteInfo useSiteInfo) + { + if (IsEmpty(ref useSiteInfo) || IncludesNull) + { + return false; + } + + if (!AnyTypeFromUnionMightMatch(_typesInUnion, type, _conversions, ref useSiteInfo)) + { + return false; + } + + if (EvaluateNodeForInputValue(_root, type, ref useSiteInfo) == false) + { + return false; + } + + // Nothing else can match after we exclude all instances of the 'type' from the set + return TryGetSampleType(new AndNode(_root, new NotNode(new IsTypeNode(type))), ref useSiteInfo) is null; + } + + /// + /// For debugging purposes only. + /// + public override string ToString() + { + var copy = new TypeUnionValueSet(_typesInUnion, _root, _conversions); + string prefix = ""; + + var discardedInfo = CompoundUseSiteInfo.Discarded; + if (copy.IsEmpty(ref discardedInfo)) + { + prefix += "Empty: "; + } + + if (_root == (object)IsTrueNode.Instance) + { + prefix += "AllValues: "; + } + + return prefix + _root.ToString(); + } + + /// + /// Base class for nodes in the logical tree defining values contained in the set. + /// + private abstract class Node + { + /// + /// For debugging purposes only. + /// + public abstract override string ToString(); + } + + private sealed class IsTypeNode(TypeSymbol type) : Node + { + public TypeSymbol Type { get; } = type; + + public sealed override string ToString() + { + return Type.ToDisplayString(); + } + } + + private sealed class IsNullNode : Node + { + public static readonly IsNullNode Instance = new IsNullNode(); + private IsNullNode() { } + + public override string ToString() + { + return "null"; + } + } + + /// + /// Can be used only as a root. + /// + private sealed class IsTrueNode : Node + { + public static readonly IsTrueNode Instance = new IsTrueNode(); + private IsTrueNode() { } + + public override string ToString() + { + return "true"; + } + } + + /// + /// Can be used only as a root. + /// + private sealed class IsFalseNode : Node + { + public static readonly IsFalseNode Instance = new IsFalseNode(); + private IsFalseNode() { } + + public override string ToString() + { + return "false"; + } + } + + private abstract class BinaryNode : Node + { + public Node Left { get; } + public Node Right { get; } + + public BinaryNode(Node left, Node right) + { + Debug.Assert(left is not (IsTrueNode or IsFalseNode)); + Debug.Assert(right is not (IsTrueNode or IsFalseNode)); + Left = left; + Right = right; + } + + public override string ToString() + { + return "(" + Left.ToString() + (this is AndNode ? " & " : " | ") + Right.ToString() + ")"; + } + } + + private sealed class AndNode(Node left, Node right) : BinaryNode(left, right) + { + } + + private sealed class OrNode(Node left, Node right) : BinaryNode(left, right) + { + } + + private sealed class NotNode : Node + { + public Node Negated { get; } + + public NotNode(Node negated) + { + Debug.Assert(negated is not (IsTrueNode or IsFalseNode)); + Negated = negated; + } + + public override string ToString() + { + return "!(" + Negated.ToString() + ")"; + } + } + } +} diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.BoolValueSet.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.BoolValueSet.cs index 97004c6966e9..22b3ff0909b4 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.BoolValueSet.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.BoolValueSet.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.CSharp internal static partial class ValueSetFactory { - private sealed class BoolValueSet : IValueSet + private sealed class BoolValueSet : IConstantValueSet { private readonly bool _hasFalse, _hasTrue; @@ -36,9 +36,9 @@ public static BoolValueSet Create(bool hasFalse, bool hasTrue) } } - bool IValueSet.IsEmpty => !_hasFalse && !_hasTrue; + bool IConstantValueSet.IsEmpty => !_hasFalse && !_hasTrue; - ConstantValue IValueSet.Sample => ConstantValue.Create(_hasTrue ? true : _hasFalse ? false : throw new ArgumentException()); + ConstantValue IConstantValueSet.Sample => ConstantValue.Create(_hasTrue ? true : _hasFalse ? false : throw new ArgumentException()); public bool Any(BinaryOperatorKind relation, bool value) { @@ -53,7 +53,7 @@ public bool Any(BinaryOperatorKind relation, bool value) } } - bool IValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, value.BooleanValue); + bool IConstantValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, value.BooleanValue); public bool All(BinaryOperatorKind relation, bool value) { @@ -68,13 +68,13 @@ public bool All(BinaryOperatorKind relation, bool value) } } - bool IValueSet.All(BinaryOperatorKind relation, ConstantValue value) => !value.IsBad && All(relation, value.BooleanValue); + bool IConstantValueSet.All(BinaryOperatorKind relation, ConstantValue value) => !value.IsBad && All(relation, value.BooleanValue); - public IValueSet Complement() => Create(!_hasFalse, !_hasTrue); + public IConstantValueSet Complement() => Create(!_hasFalse, !_hasTrue); IValueSet IValueSet.Complement() => this.Complement(); - public IValueSet Intersect(IValueSet other) + public IConstantValueSet Intersect(IConstantValueSet other) { if (this == other) return this; @@ -82,9 +82,9 @@ public IValueSet Intersect(IValueSet other) return Create(hasFalse: this._hasFalse & o._hasFalse, hasTrue: this._hasTrue & o._hasTrue); } - public IValueSet Intersect(IValueSet other) => this.Intersect((IValueSet)other); + public IValueSet Intersect(IValueSet other) => this.Intersect((IConstantValueSet)other); - public IValueSet Union(IValueSet other) + public IConstantValueSet Union(IConstantValueSet other) { if (this == other) return this; @@ -92,7 +92,7 @@ public IValueSet Union(IValueSet other) return Create(hasFalse: this._hasFalse | o._hasFalse, hasTrue: this._hasTrue | o._hasTrue); } - IValueSet IValueSet.Union(IValueSet other) => this.Union((IValueSet)other); + IValueSet IValueSet.Union(IValueSet other) => this.Union((IConstantValueSet)other); // Since we cache all distinct boolean value sets, we can use reference equality. public override bool Equals(object? obj) => this == obj; diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.BoolValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.BoolValueSetFactory.cs index b53a2eaffa48..8102d8d4c9af 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.BoolValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.BoolValueSetFactory.cs @@ -15,17 +15,17 @@ internal static partial class ValueSetFactory /// /// A value set factory for boolean values. /// - private sealed class BoolValueSetFactory : IValueSetFactory + private sealed class BoolValueSetFactory : IConstantValueSetFactory { public static readonly BoolValueSetFactory Instance = new BoolValueSetFactory(); private BoolValueSetFactory() { } - IValueSet IValueSetFactory.AllValues => BoolValueSet.AllValues; + IConstantValueSet IConstantValueSetFactory.AllValues => BoolValueSet.AllValues; - IValueSet IValueSetFactory.NoValues => BoolValueSet.None; + IConstantValueSet IConstantValueSetFactory.NoValues => BoolValueSet.None; - public IValueSet Related(BinaryOperatorKind relation, bool value) + public IConstantValueSet Related(BinaryOperatorKind relation, bool value) { switch (relation, value) { @@ -39,7 +39,7 @@ public IValueSet Related(BinaryOperatorKind relation, bool value) } } - IValueSet IValueSetFactory.Random(int expectedSize, Random random) => random.Next(4) switch + IConstantValueSet IConstantValueSetFactory.Random(int expectedSize, Random random) => random.Next(4) switch { 0 => BoolValueSet.None, 1 => BoolValueSet.OnlyFalse, @@ -48,14 +48,14 @@ public IValueSet Related(BinaryOperatorKind relation, bool value) _ => throw ExceptionUtilities.UnexpectedValue("random"), }; - ConstantValue IValueSetFactory.RandomValue(Random random) => ConstantValue.Create(random.NextDouble() < 0.5); + ConstantValue IConstantValueSetFactory.RandomValue(Random random) => ConstantValue.Create(random.NextDouble() < 0.5); - IValueSet IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) + IConstantValueSet IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) { return value.IsBad ? BoolValueSet.AllValues : Related(relation, value.BooleanValue); } - bool IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) + bool IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) { Debug.Assert(relation == BinaryOperatorKind.Equal); return left.IsBad || right.IsBad || left.BooleanValue == right.BooleanValue; diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.DecimalValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.DecimalValueSetFactory.cs index 7de64cb191c5..716a1155f1aa 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.DecimalValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.DecimalValueSetFactory.cs @@ -8,26 +8,26 @@ namespace Microsoft.CodeAnalysis.CSharp { internal static partial class ValueSetFactory { - private sealed class DecimalValueSetFactory : IValueSetFactory, IValueSetFactory + private sealed class DecimalValueSetFactory : IConstantValueSetFactory, IConstantValueSetFactory { public static readonly DecimalValueSetFactory Instance = new DecimalValueSetFactory(); - private readonly IValueSetFactory _underlying = new NumericValueSetFactory(DecimalTC.Instance); + private readonly IConstantValueSetFactory _underlying = new NumericValueSetFactory(DecimalTC.Instance); - IValueSet IValueSetFactory.AllValues => NumericValueSet.AllValues(DecimalTC.Instance); + IConstantValueSet IConstantValueSetFactory.AllValues => NumericValueSet.AllValues(DecimalTC.Instance); - IValueSet IValueSetFactory.NoValues => NumericValueSet.NoValues(DecimalTC.Instance); + IConstantValueSet IConstantValueSetFactory.NoValues => NumericValueSet.NoValues(DecimalTC.Instance); - public IValueSet Related(BinaryOperatorKind relation, decimal value) => _underlying.Related(relation, DecimalTC.Normalize(value)); + public IConstantValueSet Related(BinaryOperatorKind relation, decimal value) => _underlying.Related(relation, DecimalTC.Normalize(value)); - IValueSet IValueSetFactory.Random(int expectedSize, Random random) => _underlying.Random(expectedSize, random); + IConstantValueSet IConstantValueSetFactory.Random(int expectedSize, Random random) => _underlying.Random(expectedSize, random); - ConstantValue IValueSetFactory.RandomValue(Random random) => ConstantValue.Create(DecimalTC.Instance.Random(random)); + ConstantValue IConstantValueSetFactory.RandomValue(Random random) => ConstantValue.Create(DecimalTC.Instance.Random(random)); - IValueSet IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => + IConstantValueSet IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => value.IsBad ? NumericValueSet.AllValues(DecimalTC.Instance) : Related(relation, DecimalTC.Instance.FromConstantValue(value)); - bool IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) => _underlying.Related(relation, left, right); + bool IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) => _underlying.Related(relation, left, right); } } } diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.EnumeratedValueSet.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.EnumeratedValueSet.cs index 6e9de4e87dc5..c3c3a5b77923 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.EnumeratedValueSet.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.EnumeratedValueSet.cs @@ -18,7 +18,7 @@ internal static partial class ValueSetFactory /// relational operators for it; such a set can be formed only by including explicitly mentioned /// members (or the inverse, excluding them, by complementing the set). /// - private sealed class EnumeratedValueSet : IValueSet + private sealed class EnumeratedValueSet : IConstantValueSet where T : notnull { /// @@ -45,7 +45,7 @@ internal static EnumeratedValueSet Including(T value, IEquatableValueTC tc public bool IsEmpty => _included && _membersIncludedOrExcluded.IsEmpty; - ConstantValue IValueSet.Sample + ConstantValue IConstantValueSet.Sample { get { @@ -85,7 +85,7 @@ public bool Any(BinaryOperatorKind relation, T value) } } - bool IValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, _tc.FromConstantValue(value)); + bool IConstantValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, _tc.FromConstantValue(value)); public bool All(BinaryOperatorKind relation, T value) { @@ -108,13 +108,13 @@ public bool All(BinaryOperatorKind relation, T value) } } - bool IValueSet.All(BinaryOperatorKind relation, ConstantValue value) => !value.IsBad && All(relation, _tc.FromConstantValue(value)); + bool IConstantValueSet.All(BinaryOperatorKind relation, ConstantValue value) => !value.IsBad && All(relation, _tc.FromConstantValue(value)); - public IValueSet Complement() => new EnumeratedValueSet(!_included, _membersIncludedOrExcluded, _tc); + public IConstantValueSet Complement() => new EnumeratedValueSet(!_included, _membersIncludedOrExcluded, _tc); IValueSet IValueSet.Complement() => this.Complement(); - public IValueSet Intersect(IValueSet o) + public IConstantValueSet Intersect(IConstantValueSet o) { if (this == o) return this; @@ -135,9 +135,9 @@ public IValueSet Intersect(IValueSet o) } } - IValueSet IValueSet.Intersect(IValueSet other) => Intersect((IValueSet)other); + IValueSet IValueSet.Intersect(IValueSet other) => Intersect((IConstantValueSet)other); - public IValueSet Union(IValueSet o) + public IConstantValueSet Union(IConstantValueSet o) { if (this == o) return this; @@ -158,7 +158,7 @@ public IValueSet Union(IValueSet o) } } - IValueSet IValueSet.Union(IValueSet other) => Union((IValueSet)other); + IValueSet IValueSet.Union(IValueSet other) => Union((IConstantValueSet)other); public override bool Equals(object? obj) { diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.EnumeratedValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.EnumeratedValueSetFactory.cs index 7bb2557e1dee..9cab0cb76c32 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.EnumeratedValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.EnumeratedValueSetFactory.cs @@ -14,17 +14,17 @@ internal static partial class ValueSetFactory /// /// A value set factory that only supports equality and works by including or excluding specific values. /// - private sealed class EnumeratedValueSetFactory : IValueSetFactory where T : notnull + private sealed class EnumeratedValueSetFactory : IConstantValueSetFactory where T : notnull { private readonly IEquatableValueTC _tc; - IValueSet IValueSetFactory.AllValues => EnumeratedValueSet.AllValues(_tc); + IConstantValueSet IConstantValueSetFactory.AllValues => EnumeratedValueSet.AllValues(_tc); - IValueSet IValueSetFactory.NoValues => EnumeratedValueSet.NoValues(_tc); + IConstantValueSet IConstantValueSetFactory.NoValues => EnumeratedValueSet.NoValues(_tc); public EnumeratedValueSetFactory(IEquatableValueTC tc) { _tc = tc; } - public IValueSet Related(BinaryOperatorKind relation, T value) + public IConstantValueSet Related(BinaryOperatorKind relation, T value) { switch (relation) { @@ -35,19 +35,19 @@ public IValueSet Related(BinaryOperatorKind relation, T value) } } - IValueSet IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => + IConstantValueSet IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || value.IsNull ? EnumeratedValueSet.AllValues(_tc) : this.Related(relation, _tc.FromConstantValue(value)); - bool IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) + bool IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) { Debug.Assert(relation == BinaryOperatorKind.Equal); return _tc.FromConstantValue(left).Equals(_tc.FromConstantValue(right)); } - public IValueSet Random(int expectedSize, Random random) + public IConstantValueSet Random(int expectedSize, Random random) { T[] values = _tc.RandomValues(expectedSize, random, expectedSize * 2); - IValueSet result = EnumeratedValueSet.NoValues(_tc); + IConstantValueSet result = EnumeratedValueSet.NoValues(_tc); Debug.Assert(result.IsEmpty); foreach (T value in values) result = result.Union(Related(Equal, value)); @@ -55,7 +55,7 @@ public IValueSet Random(int expectedSize, Random random) return result; } - ConstantValue IValueSetFactory.RandomValue(Random random) + ConstantValue IConstantValueSetFactory.RandomValue(Random random) { return _tc.ToConstantValue(_tc.RandomValues(1, random, 100)[0]); } diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.FloatingValueSet.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.FloatingValueSet.cs index 5790692fd6c9..f63dca0dd0d5 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.FloatingValueSet.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.FloatingValueSet.cs @@ -17,25 +17,25 @@ internal static partial class ValueSetFactory /// A value set implementation for and . /// /// A floating-point type. - private sealed class FloatingValueSet : IValueSet + private sealed class FloatingValueSet : IConstantValueSet { - private readonly IValueSet _numbers; + private readonly IConstantValueSet _numbers; private readonly bool _hasNaN; private readonly FloatingTC _tc; - private FloatingValueSet(IValueSet numbers, bool hasNaN, FloatingTC tc) + private FloatingValueSet(IConstantValueSet numbers, bool hasNaN, FloatingTC tc) { RoslynDebug.Assert(numbers is NumericValueSet); (_numbers, _hasNaN, _tc) = (numbers, hasNaN, tc); } - internal static IValueSet AllValues(FloatingTC tc) => new FloatingValueSet( + internal static IConstantValueSet AllValues(FloatingTC tc) => new FloatingValueSet( numbers: NumericValueSet.AllValues(tc), hasNaN: true, tc); - internal static IValueSet NoValues(FloatingTC tc) => new FloatingValueSet( + internal static IConstantValueSet NoValues(FloatingTC tc) => new FloatingValueSet( numbers: NumericValueSet.NoValues(tc), hasNaN: false, tc); - internal static IValueSet Random(int expectedSize, Random random, FloatingTC tc) + internal static IConstantValueSet Random(int expectedSize, Random random, FloatingTC tc) { bool hasNan = random.NextDouble() < 0.5; if (hasNan) @@ -43,12 +43,12 @@ internal static IValueSet Random(int expectedSize, Random random, Flo if (expectedSize < 1) expectedSize = 2; return new FloatingValueSet( - numbers: (IValueSet)new NumericValueSetFactory(tc).Random(expectedSize, random), hasNaN: hasNan, tc); + numbers: (IConstantValueSet)new NumericValueSetFactory(tc).Random(expectedSize, random), hasNaN: hasNan, tc); } public bool IsEmpty => !_hasNaN && _numbers.IsEmpty; - ConstantValue IValueSet.Sample + ConstantValue IConstantValueSet.Sample { get { @@ -67,7 +67,7 @@ ConstantValue IValueSet.Sample } } - public static IValueSet Related(BinaryOperatorKind relation, TFloating value, FloatingTC tc) + public static IConstantValueSet Related(BinaryOperatorKind relation, TFloating value, FloatingTC tc) { if (tc.Related(Equal, tc.NaN, value)) { @@ -95,7 +95,7 @@ public static IValueSet Related(BinaryOperatorKind relation, TFloatin ); } - public IValueSet Intersect(IValueSet o) + public IConstantValueSet Intersect(IConstantValueSet o) { if (this == o) return this; @@ -108,9 +108,9 @@ public IValueSet Intersect(IValueSet o) _tc); } - IValueSet IValueSet.Intersect(IValueSet other) => this.Intersect((IValueSet)other); + IValueSet IValueSet.Intersect(IValueSet other) => this.Intersect((IConstantValueSet)other); - public IValueSet Union(IValueSet o) + public IConstantValueSet Union(IConstantValueSet o) { if (this == o) return this; @@ -123,9 +123,9 @@ public IValueSet Union(IValueSet o) _tc); } - IValueSet IValueSet.Union(IValueSet other) => this.Union((IValueSet)other); + IValueSet IValueSet.Union(IValueSet other) => this.Union((IConstantValueSet)other); - public IValueSet Complement() + public IConstantValueSet Complement() { return new FloatingValueSet( numbers: this._numbers.Complement(), @@ -135,7 +135,7 @@ public IValueSet Complement() IValueSet IValueSet.Complement() => this.Complement(); - bool IValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => + bool IConstantValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || this.Any(relation, _tc.FromConstantValue(value)); public bool Any(BinaryOperatorKind relation, TFloating value) @@ -145,7 +145,7 @@ public bool Any(BinaryOperatorKind relation, TFloating value) _numbers.Any(relation, value); } - bool IValueSet.All(BinaryOperatorKind relation, ConstantValue value) => !value.IsBad && All(relation, _tc.FromConstantValue(value)); + bool IConstantValueSet.All(BinaryOperatorKind relation, ConstantValue value) => !value.IsBad && All(relation, _tc.FromConstantValue(value)); public bool All(BinaryOperatorKind relation, TFloating value) { diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.FloatingValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.FloatingValueSetFactory.cs index 0ca18637eecb..7d91a55e25a2 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.FloatingValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.FloatingValueSetFactory.cs @@ -8,7 +8,7 @@ namespace Microsoft.CodeAnalysis.CSharp { internal static partial class ValueSetFactory { - private sealed class FloatingValueSetFactory : IValueSetFactory + private sealed class FloatingValueSetFactory : IConstantValueSetFactory { private readonly FloatingTC _tc; @@ -17,27 +17,27 @@ public FloatingValueSetFactory(FloatingTC tc) _tc = tc; } - IValueSet IValueSetFactory.AllValues => FloatingValueSet.AllValues(_tc); + IConstantValueSet IConstantValueSetFactory.AllValues => FloatingValueSet.AllValues(_tc); - IValueSet IValueSetFactory.NoValues => FloatingValueSet.NoValues(_tc); + IConstantValueSet IConstantValueSetFactory.NoValues => FloatingValueSet.NoValues(_tc); - public IValueSet Related(BinaryOperatorKind relation, TFloating value) => + public IConstantValueSet Related(BinaryOperatorKind relation, TFloating value) => FloatingValueSet.Related(relation, value, _tc); - IValueSet IValueSetFactory.Random(int expectedSize, Random random) => + IConstantValueSet IConstantValueSetFactory.Random(int expectedSize, Random random) => FloatingValueSet.Random(expectedSize, random, _tc); - ConstantValue IValueSetFactory.RandomValue(Random random) + ConstantValue IConstantValueSetFactory.RandomValue(Random random) { return _tc.ToConstantValue(_tc.Random(random)); } - IValueSet IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => + IConstantValueSet IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => value.IsBad ? FloatingValueSet.AllValues(_tc) : FloatingValueSet.Related(relation, _tc.FromConstantValue(value), _tc); - bool IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) + bool IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) { return _tc.Related(relation, _tc.FromConstantValue(left), _tc.FromConstantValue(right)); } diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NintValueSet.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NintValueSet.cs index eef76f859520..a88d7daff4e6 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NintValueSet.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NintValueSet.cs @@ -12,13 +12,13 @@ namespace Microsoft.CodeAnalysis.CSharp internal static partial class ValueSetFactory { - private sealed class NintValueSet : IValueSet, IValueSet + private sealed class NintValueSet : IConstantValueSet, IValueSet { public static readonly NintValueSet AllValues = new NintValueSet(hasSmall: true, values: NumericValueSet.AllValues(IntTC.DefaultInstance), hasLarge: true); public static readonly NintValueSet NoValues = new NintValueSet(hasSmall: false, values: NumericValueSet.NoValues(IntTC.DefaultInstance), hasLarge: false); - private readonly IValueSet _values; + private readonly IConstantValueSet _values; /// /// A value of type nint may, in a 64-bit runtime, take on values less than . @@ -36,7 +36,7 @@ private sealed class NintValueSet : IValueSet, IValueSet /// private readonly bool _hasLarge; - internal NintValueSet(bool hasSmall, IValueSet values, bool hasLarge) + internal NintValueSet(bool hasSmall, IConstantValueSet values, bool hasLarge) { _hasSmall = hasSmall; _values = values; @@ -45,7 +45,7 @@ internal NintValueSet(bool hasSmall, IValueSet values, bool hasLarge) public bool IsEmpty => !_hasSmall && !_hasLarge && _values.IsEmpty; - ConstantValue? IValueSet.Sample + ConstantValue? IConstantValueSet.Sample { get { @@ -71,7 +71,7 @@ public bool All(BinaryOperatorKind relation, int value) return _values.All(relation, value); } - bool IValueSet.All(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || All(relation, value.Int32Value); + bool IConstantValueSet.All(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || All(relation, value.Int32Value); public bool Any(BinaryOperatorKind relation, int value) { @@ -82,9 +82,9 @@ public bool Any(BinaryOperatorKind relation, int value) return _values.Any(relation, value); } - bool IValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, value.Int32Value); + bool IConstantValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, value.Int32Value); - public IValueSet Complement() + public IConstantValueSet Complement() { return new NintValueSet( hasSmall: !this._hasSmall, @@ -95,7 +95,7 @@ public IValueSet Complement() IValueSet IValueSet.Complement() => this.Complement(); - public IValueSet Intersect(IValueSet o) + public IConstantValueSet Intersect(IConstantValueSet o) { var other = (NintValueSet)o; return new NintValueSet( @@ -107,7 +107,7 @@ public IValueSet Intersect(IValueSet o) IValueSet IValueSet.Intersect(IValueSet other) => this.Intersect((NintValueSet)other); - public IValueSet Union(IValueSet o) + public IConstantValueSet Union(IConstantValueSet o) { var other = (NintValueSet)o; return new NintValueSet( diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NintValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NintValueSetFactory.cs index 0d4cc21750cb..976574b17a0a 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NintValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NintValueSetFactory.cs @@ -10,17 +10,17 @@ namespace Microsoft.CodeAnalysis.CSharp internal static partial class ValueSetFactory { - private sealed class NintValueSetFactory : IValueSetFactory, IValueSetFactory + private sealed class NintValueSetFactory : IConstantValueSetFactory, IConstantValueSetFactory { public static readonly NintValueSetFactory Instance = new NintValueSetFactory(); private NintValueSetFactory() { } - IValueSet IValueSetFactory.AllValues => NintValueSet.AllValues; + IConstantValueSet IConstantValueSetFactory.AllValues => NintValueSet.AllValues; - IValueSet IValueSetFactory.NoValues => NintValueSet.NoValues; + IConstantValueSet IConstantValueSetFactory.NoValues => NintValueSet.NoValues; - public IValueSet Related(BinaryOperatorKind relation, int value) + public IConstantValueSet Related(BinaryOperatorKind relation, int value) { return new NintValueSet( hasSmall: relation switch { LessThan => true, LessThanOrEqual => true, _ => false }, @@ -29,23 +29,23 @@ public IValueSet Related(BinaryOperatorKind relation, int value) ); } - IValueSet IValueSetFactory.Random(int expectedSize, Random random) + IConstantValueSet IConstantValueSetFactory.Random(int expectedSize, Random random) { return new NintValueSet( hasSmall: random.NextDouble() < 0.25, - values: (IValueSet)new NumericValueSetFactory(IntTC.DefaultInstance).Random(expectedSize, random), + values: (IConstantValueSet)new NumericValueSetFactory(IntTC.DefaultInstance).Random(expectedSize, random), hasLarge: random.NextDouble() < 0.25 ); } - ConstantValue IValueSetFactory.RandomValue(Random random) => ConstantValue.CreateNativeInt(IntTC.DefaultInstance.Random(random)); + ConstantValue IConstantValueSetFactory.RandomValue(Random random) => ConstantValue.CreateNativeInt(IntTC.DefaultInstance.Random(random)); - IValueSet IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) + IConstantValueSet IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) { return value.IsBad ? NintValueSet.AllValues : Related(relation, IntTC.DefaultInstance.FromConstantValue(value)); } - bool IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) + bool IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) { var tc = IntTC.DefaultInstance; return tc.Related(relation, tc.FromConstantValue(left), tc.FromConstantValue(right)); diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NonNegativeIntValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NonNegativeIntValueSetFactory.cs index 4697b463ecff..39744432a9a9 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NonNegativeIntValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NonNegativeIntValueSetFactory.cs @@ -11,18 +11,18 @@ namespace Microsoft.CodeAnalysis.CSharp internal static partial class ValueSetFactory { - private sealed class NonNegativeIntValueSetFactory : IValueSetFactory + private sealed class NonNegativeIntValueSetFactory : IConstantValueSetFactory { public static readonly NonNegativeIntValueSetFactory Instance = new NonNegativeIntValueSetFactory(); - private static readonly IValueSetFactory s_underlying = new NumericValueSetFactory(IntTC.NonNegativeInstance); + private static readonly IConstantValueSetFactory s_underlying = new NumericValueSetFactory(IntTC.NonNegativeInstance); private NonNegativeIntValueSetFactory() { } - public IValueSet AllValues => NumericValueSet.AllValues(IntTC.NonNegativeInstance); + public IConstantValueSet AllValues => NumericValueSet.AllValues(IntTC.NonNegativeInstance); - public IValueSet NoValues => NumericValueSet.NoValues(IntTC.NonNegativeInstance); + public IConstantValueSet NoValues => NumericValueSet.NoValues(IntTC.NonNegativeInstance); - public IValueSet Related(BinaryOperatorKind relation, int value) + public IConstantValueSet Related(BinaryOperatorKind relation, int value) { var tc = IntTC.NonNegativeInstance; switch (relation) @@ -50,14 +50,14 @@ public IValueSet Related(BinaryOperatorKind relation, int value) } } - IValueSet IValueSetFactory.Random(int expectedSize, Random random) => s_underlying.Random(expectedSize, random); + IConstantValueSet IConstantValueSetFactory.Random(int expectedSize, Random random) => s_underlying.Random(expectedSize, random); - ConstantValue IValueSetFactory.RandomValue(Random random) => s_underlying.RandomValue(random); + ConstantValue IConstantValueSetFactory.RandomValue(Random random) => s_underlying.RandomValue(random); - IValueSet IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => + IConstantValueSet IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => value.IsBad ? AllValues : Related(relation, IntTC.NonNegativeInstance.FromConstantValue(value)); - bool IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) => s_underlying.Related(relation, left, right); + bool IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) => s_underlying.Related(relation, left, right); } } } diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NuintValueSet.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NuintValueSet.cs index f22ec1267e0f..b99528bc59b6 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NuintValueSet.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NuintValueSet.cs @@ -12,13 +12,13 @@ namespace Microsoft.CodeAnalysis.CSharp internal static partial class ValueSetFactory { - private sealed class NuintValueSet : IValueSet, IValueSet + private sealed class NuintValueSet : IConstantValueSet, IValueSet { public static readonly NuintValueSet AllValues = new NuintValueSet(values: NumericValueSet.AllValues(UIntTC.Instance), hasLarge: true); public static readonly NuintValueSet NoValues = new NuintValueSet(values: NumericValueSet.NoValues(UIntTC.Instance), hasLarge: false); - private readonly IValueSet _values; + private readonly IConstantValueSet _values; /// /// A value of type nuint may, in a 64-bit runtime, take on values greater than . @@ -28,7 +28,7 @@ private sealed class NuintValueSet : IValueSet, IValueSet /// private readonly bool _hasLarge; - internal NuintValueSet(IValueSet values, bool hasLarge) + internal NuintValueSet(IConstantValueSet values, bool hasLarge) { _values = values; _hasLarge = hasLarge; @@ -36,7 +36,7 @@ internal NuintValueSet(IValueSet values, bool hasLarge) public bool IsEmpty => !_hasLarge && _values.IsEmpty; - ConstantValue? IValueSet.Sample + ConstantValue? IConstantValueSet.Sample { get { @@ -60,7 +60,7 @@ public bool All(BinaryOperatorKind relation, uint value) return _values.All(relation, value); } - bool IValueSet.All(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || All(relation, value.UInt32Value); + bool IConstantValueSet.All(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || All(relation, value.UInt32Value); public bool Any(BinaryOperatorKind relation, uint value) { @@ -69,9 +69,9 @@ public bool Any(BinaryOperatorKind relation, uint value) return _values.Any(relation, value); } - bool IValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, value.UInt32Value); + bool IConstantValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, value.UInt32Value); - public IValueSet Complement() + public IConstantValueSet Complement() { return new NuintValueSet( values: this._values.Complement(), @@ -81,7 +81,7 @@ public IValueSet Complement() IValueSet IValueSet.Complement() => this.Complement(); - public IValueSet Intersect(IValueSet o) + public IConstantValueSet Intersect(IConstantValueSet o) { var other = (NuintValueSet)o; return new NuintValueSet( @@ -92,7 +92,7 @@ public IValueSet Intersect(IValueSet o) IValueSet IValueSet.Intersect(IValueSet other) => this.Intersect((NuintValueSet)other); - public IValueSet Union(IValueSet o) + public IConstantValueSet Union(IConstantValueSet o) { var other = (NuintValueSet)o; return new NuintValueSet( diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NuintValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NuintValueSetFactory.cs index d0f25f6f04a9..0cad1855fb01 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NuintValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NuintValueSetFactory.cs @@ -10,17 +10,17 @@ namespace Microsoft.CodeAnalysis.CSharp internal static partial class ValueSetFactory { - private sealed class NuintValueSetFactory : IValueSetFactory, IValueSetFactory + private sealed class NuintValueSetFactory : IConstantValueSetFactory, IConstantValueSetFactory { public static readonly NuintValueSetFactory Instance = new NuintValueSetFactory(); private NuintValueSetFactory() { } - IValueSet IValueSetFactory.AllValues => NuintValueSet.AllValues; + IConstantValueSet IConstantValueSetFactory.AllValues => NuintValueSet.AllValues; - IValueSet IValueSetFactory.NoValues => NuintValueSet.NoValues; + IConstantValueSet IConstantValueSetFactory.NoValues => NuintValueSet.NoValues; - public IValueSet Related(BinaryOperatorKind relation, uint value) + public IConstantValueSet Related(BinaryOperatorKind relation, uint value) { return new NuintValueSet( values: new NumericValueSetFactory(UIntTC.Instance).Related(relation, value), @@ -28,22 +28,22 @@ public IValueSet Related(BinaryOperatorKind relation, uint value) ); } - IValueSet IValueSetFactory.Random(int expectedSize, Random random) + IConstantValueSet IConstantValueSetFactory.Random(int expectedSize, Random random) { return new NuintValueSet( - values: (IValueSet)new NumericValueSetFactory(UIntTC.Instance).Random(expectedSize, random), + values: (IConstantValueSet)new NumericValueSetFactory(UIntTC.Instance).Random(expectedSize, random), hasLarge: random.NextDouble() < 0.25 ); } - ConstantValue IValueSetFactory.RandomValue(Random random) => ConstantValue.CreateNativeUInt(UIntTC.Instance.Random(random)); + ConstantValue IConstantValueSetFactory.RandomValue(Random random) => ConstantValue.CreateNativeUInt(UIntTC.Instance.Random(random)); - IValueSet IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) + IConstantValueSet IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) { return value.IsBad ? NuintValueSet.AllValues : Related(relation, UIntTC.Instance.FromConstantValue(value)); } - bool IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) + bool IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) { var tc = UIntTC.Instance; return tc.Related(relation, tc.FromConstantValue(left), tc.FromConstantValue(right)); diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NumericValueSet.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NumericValueSet.cs index 988092ba6145..09a31274fa90 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NumericValueSet.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NumericValueSet.cs @@ -19,7 +19,7 @@ internal static partial class ValueSetFactory /// /// The implementation of a value set for an numeric type . /// - private sealed class NumericValueSet : IValueSet + private sealed class NumericValueSet : IConstantValueSet { private readonly ImmutableArray<(T first, T last)> _intervals; private readonly INumericTC _tc; @@ -53,7 +53,7 @@ internal NumericValueSet(ImmutableArray<(T first, T last)> intervals, INumericTC public bool IsEmpty => _intervals.Length == 0; - ConstantValue IValueSet.Sample + ConstantValue IConstantValueSet.Sample { get { @@ -104,7 +104,7 @@ bool anyIntervalContains(int firstIntervalIndex, int lastIntervalIndex, T value) } } - bool IValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, _tc.FromConstantValue(value)); + bool IConstantValueSet.Any(BinaryOperatorKind relation, ConstantValue value) => value.IsBad || Any(relation, _tc.FromConstantValue(value)); public bool All(BinaryOperatorKind relation, T value) { @@ -126,9 +126,9 @@ public bool All(BinaryOperatorKind relation, T value) } } - bool IValueSet.All(BinaryOperatorKind relation, ConstantValue value) => !value.IsBad && All(relation, _tc.FromConstantValue(value)); + bool IConstantValueSet.All(BinaryOperatorKind relation, ConstantValue value) => !value.IsBad && All(relation, _tc.FromConstantValue(value)); - public IValueSet Complement() + public IConstantValueSet Complement() { if (_intervals.Length == 0) return AllValues(_tc); @@ -159,7 +159,7 @@ public IValueSet Complement() IValueSet IValueSet.Complement() => this.Complement(); - public IValueSet Intersect(IValueSet o) + public IConstantValueSet Intersect(IConstantValueSet o) { var other = (NumericValueSet)o; Debug.Assert(this._tc.GetType() == other._tc.GetType()); @@ -234,9 +234,9 @@ private static T Max(T a, T b, INumericTC tc) return tc.Related(LessThan, a, b) ? b : a; } - IValueSet IValueSet.Intersect(IValueSet other) => this.Intersect((IValueSet)other); + IValueSet IValueSet.Intersect(IValueSet other) => this.Intersect((IConstantValueSet)other); - public IValueSet Union(IValueSet o) + public IConstantValueSet Union(IConstantValueSet o) { var other = (NumericValueSet)o; Debug.Assert(this._tc.GetType() == other._tc.GetType()); @@ -285,12 +285,12 @@ public IValueSet Union(IValueSet o) return new NumericValueSet(builder.ToImmutableAndFree(), _tc); } - IValueSet IValueSet.Union(IValueSet other) => this.Union((IValueSet)other); + IValueSet IValueSet.Union(IValueSet other) => this.Union((IConstantValueSet)other); /// /// Produce a random value set for testing purposes. /// - internal static IValueSet Random(int expectedSize, Random random, INumericTC tc) + internal static IConstantValueSet Random(int expectedSize, Random random, INumericTC tc) { T[] values = new T[expectedSize * 2]; for (int i = 0, n = expectedSize * 2; i < n; i++) diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NumericValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NumericValueSetFactory.cs index 1534346ee84f..1e4c16f2ea1d 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NumericValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.NumericValueSetFactory.cs @@ -16,17 +16,17 @@ internal static partial class ValueSetFactory /// parameterized by a type class /// that provides the primitives for that type. /// - private struct NumericValueSetFactory : IValueSetFactory + private struct NumericValueSetFactory : IConstantValueSetFactory { private readonly INumericTC _tc; - IValueSet IValueSetFactory.AllValues => NumericValueSet.AllValues(_tc); + IConstantValueSet IConstantValueSetFactory.AllValues => NumericValueSet.AllValues(_tc); - IValueSet IValueSetFactory.NoValues => NumericValueSet.NoValues(_tc); + IConstantValueSet IConstantValueSetFactory.NoValues => NumericValueSet.NoValues(_tc); public NumericValueSetFactory(INumericTC tc) { this._tc = tc; } - public IValueSet Related(BinaryOperatorKind relation, T value) + public IConstantValueSet Related(BinaryOperatorKind relation, T value) { switch (relation) { @@ -49,18 +49,18 @@ public IValueSet Related(BinaryOperatorKind relation, T value) } } - IValueSet IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => + IConstantValueSet IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue value) => value.IsBad ? NumericValueSet.AllValues(_tc) : Related(relation, _tc.FromConstantValue(value)); - public IValueSet Random(int expectedSize, Random random) => + public IConstantValueSet Random(int expectedSize, Random random) => NumericValueSet.Random(expectedSize, random, _tc); - ConstantValue IValueSetFactory.RandomValue(Random random) + ConstantValue IConstantValueSetFactory.RandomValue(Random random) { return _tc.ToConstantValue(_tc.Random(random)); } - bool IValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) + bool IConstantValueSetFactory.Related(BinaryOperatorKind relation, ConstantValue left, ConstantValue right) { return _tc.Related(relation, _tc.FromConstantValue(left), _tc.FromConstantValue(right)); } diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.UnionTypeTypeUnionValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.UnionTypeTypeUnionValueSetFactory.cs new file mode 100644 index 000000000000..77528567c873 --- /dev/null +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.UnionTypeTypeUnionValueSetFactory.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Symbols; + +namespace Microsoft.CodeAnalysis.CSharp +{ + internal static partial class ValueSetFactory + { + private sealed class UnionTypeTypeUnionValueSetFactory : ITypeUnionValueSetFactory + { + private readonly NamedTypeSymbol _unionType; + + public UnionTypeTypeUnionValueSetFactory(NamedTypeSymbol unionType) + { + Debug.Assert(unionType is NamedTypeSymbol { IsUnionType: true }); + _unionType = unionType; + } + + private ImmutableArray AdjustedTypesInUnion() + { + Debug.Assert(!_unionType.UnionCaseTypes.Any(t => t.IsNullableType())); + return _unionType.UnionCaseTypes; + } + + public TypeUnionValueSet AllValues(ConversionsBase conversions) + { + return TypeUnionValueSet.AllValues(AdjustedTypesInUnion(), conversions); + } + + public TypeUnionValueSet FromTypeMatch(TypeSymbol type, ConversionsBase conversions, ref CompoundUseSiteInfo useSiteInfo) + { + return TypeUnionValueSet.FromTypeMatch(AdjustedTypesInUnion(), type, conversions, ref useSiteInfo); + } + + public TypeUnionValueSet FromNullMatch(ConversionsBase conversions) + { + return TypeUnionValueSet.FromNullMatch(AdjustedTypesInUnion(), conversions); + } + + public TypeUnionValueSet FromNonNullMatch(ConversionsBase conversions) + { + return TypeUnionValueSet.FromNonNullMatch(AdjustedTypesInUnion(), conversions); + } + } + } +} diff --git a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.cs b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.cs index 9282148453f6..06c5a4e2e7a9 100644 --- a/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.cs +++ b/src/Compilers/CSharp/Portable/Utilities/ValueSetFactory.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Symbols; namespace Microsoft.CodeAnalysis.CSharp @@ -11,25 +12,25 @@ namespace Microsoft.CodeAnalysis.CSharp /// internal static partial class ValueSetFactory { - internal static readonly IValueSetFactory ForByte = new NumericValueSetFactory(ByteTC.Instance); - internal static readonly IValueSetFactory ForSByte = new NumericValueSetFactory(SByteTC.Instance); - internal static readonly IValueSetFactory ForChar = new NumericValueSetFactory(CharTC.Instance); - internal static readonly IValueSetFactory ForShort = new NumericValueSetFactory(ShortTC.Instance); - internal static readonly IValueSetFactory ForUShort = new NumericValueSetFactory(UShortTC.Instance); - internal static readonly IValueSetFactory ForInt = new NumericValueSetFactory(IntTC.DefaultInstance); - internal static readonly IValueSetFactory ForUInt = new NumericValueSetFactory(UIntTC.Instance); - internal static readonly IValueSetFactory ForLong = new NumericValueSetFactory(LongTC.Instance); - internal static readonly IValueSetFactory ForULong = new NumericValueSetFactory(ULongTC.Instance); - internal static readonly IValueSetFactory ForBool = BoolValueSetFactory.Instance; - internal static readonly IValueSetFactory ForFloat = new FloatingValueSetFactory(SingleTC.Instance); - internal static readonly IValueSetFactory ForDouble = new FloatingValueSetFactory(DoubleTC.Instance); - internal static readonly IValueSetFactory ForString = new EnumeratedValueSetFactory(StringTC.Instance); - internal static readonly IValueSetFactory ForDecimal = DecimalValueSetFactory.Instance; - internal static readonly IValueSetFactory ForNint = NintValueSetFactory.Instance; - internal static readonly IValueSetFactory ForNuint = NuintValueSetFactory.Instance; - internal static readonly IValueSetFactory ForLength = NonNegativeIntValueSetFactory.Instance; + internal static readonly IConstantValueSetFactory ForByte = new NumericValueSetFactory(ByteTC.Instance); + internal static readonly IConstantValueSetFactory ForSByte = new NumericValueSetFactory(SByteTC.Instance); + internal static readonly IConstantValueSetFactory ForChar = new NumericValueSetFactory(CharTC.Instance); + internal static readonly IConstantValueSetFactory ForShort = new NumericValueSetFactory(ShortTC.Instance); + internal static readonly IConstantValueSetFactory ForUShort = new NumericValueSetFactory(UShortTC.Instance); + internal static readonly IConstantValueSetFactory ForInt = new NumericValueSetFactory(IntTC.DefaultInstance); + internal static readonly IConstantValueSetFactory ForUInt = new NumericValueSetFactory(UIntTC.Instance); + internal static readonly IConstantValueSetFactory ForLong = new NumericValueSetFactory(LongTC.Instance); + internal static readonly IConstantValueSetFactory ForULong = new NumericValueSetFactory(ULongTC.Instance); + internal static readonly IConstantValueSetFactory ForBool = BoolValueSetFactory.Instance; + internal static readonly IConstantValueSetFactory ForFloat = new FloatingValueSetFactory(SingleTC.Instance); + internal static readonly IConstantValueSetFactory ForDouble = new FloatingValueSetFactory(DoubleTC.Instance); + internal static readonly IConstantValueSetFactory ForString = new EnumeratedValueSetFactory(StringTC.Instance); + internal static readonly IConstantValueSetFactory ForDecimal = DecimalValueSetFactory.Instance; + internal static readonly IConstantValueSetFactory ForNint = NintValueSetFactory.Instance; + internal static readonly IConstantValueSetFactory ForNuint = NuintValueSetFactory.Instance; + internal static readonly IConstantValueSetFactory ForLength = NonNegativeIntValueSetFactory.Instance; - public static IValueSetFactory? ForSpecialType(SpecialType specialType, bool isNative = false) + public static IConstantValueSetFactory? ForSpecialType(SpecialType specialType, bool isNative = false) { return specialType switch { @@ -53,7 +54,7 @@ internal static partial class ValueSetFactory }; } - public static IValueSetFactory? ForType(TypeSymbol type) + public static IConstantValueSetFactory? ForType(TypeSymbol type) { if (type.IsSpanOrReadOnlySpanChar()) return ForString; @@ -61,11 +62,21 @@ internal static partial class ValueSetFactory return ForSpecialType(type.SpecialType, type.IsNativeIntegerType); } - public static IValueSetFactory? ForInput(BoundDagTemp input) + public static IConstantValueSetFactory? ForInput(BoundDagTemp input) { if (input.Source is BoundDagPropertyEvaluation { IsLengthOrCount: true }) return ForLength; return ForType(input.Type); } + + public static ITypeUnionValueSetFactory? TypeUnionValueSetFactoryForInput(BoundDagTemp input) + { + if (DecisionDagBuilder.IsUnionValue(input, out BoundDagTemp? unionInstance) && ((NamedTypeSymbol)unionInstance.Type).UnionCaseTypes is not []) + { + return new UnionTypeTypeUnionValueSetFactory((NamedTypeSymbol)unionInstance.Type); + } + + return null; + } } } diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf index f60da5ebe303..12d5dec42747 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.cs.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + Žádné přetížení pro metodu {0} nepřijímá argumenty elementu with(...) {1} @@ -387,6 +387,21 @@ Atribut AsyncMethodBuilder je u anonymních metod bez explicitního návratového typu zakázaný. + + Unsafe member '{0}' cannot implement safe member '{1}' + Nebezpečný člen {0} nemůže implementovat bezpečný člen {1}. + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + Nebezpečný člen {0} nemůže implicitně implementovat bezpečný člen {1}. + + + + Unsafe member '{0}' cannot override safe member '{1}' + Nebezpečný člen {0} nemůže přepsat bezpečný člen {1}. + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. U tohoto členu nelze použít OverloadResolutionPriorityAttribute. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + Argumenty elementu with(...) nemůžou být dynamické 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + Element with(...) pro rozhraní jen pro čtení musí být prázdný, pokud je k dispozici 'with(...)' element must be the first element - 'with(...)' element must be the first element + Element with(...) musí být prvním elementem 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + Elementy with(...) nejsou pro typ {0} podporovány @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. + Nelze najít přístupnou metodu {0} s očekávanou signaturou: statickou metodu, jejíž poslední parametr je typu ReadOnlySpan<{1}> a návratového typu {2}. @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + Typ elementu této kolekce nesmí být ref struct nebo parametr typu umožňující ref structs + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + Neplatná hodnota {0}: {1} pro jazyk C# {2}. Použijte prosím verzi jazyka {3} nebo vyšší. @@ -902,6 +922,11 @@ Strom výrazů nesmí obsahovat operátor řazené kolekce členů == nebo !=. + + An expression tree may not contain a union conversion. + Strom výrazů nesmí obsahovat převod typu union. + + An expression tree may not contain a with-expression. Strom výrazů nesmí obsahovat výraz with. @@ -1237,6 +1262,16 @@ Pole vloženého prvku pole nelze deklarovat jako povinné, jen pro čtení, nestálé nebo jako vyrovnávací paměť s pevnou velikostí. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + Explicitně deklarované veřejné konstruktory s jedním parametrem nejsou v deklaraci union povolené. + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + Pole instance, automatické vlastnosti ani události podobné polím nejsou v deklaraci union povoleny. + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter {0}: Nelze deklarovat členy instance v bloku rozšíření s nepojmenovaným parametrem příjemce. @@ -1467,6 +1502,16 @@ {0} neobsahuje definici pro {1} a nebyla nalezena žádná přístupná metoda rozšíření {1}, která by přijímala první argument typu {0}. (Nechtěli jste místo toho iterovat asynchronní kolekci pomocí await foreach?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + Typ {0} nelze převést na object pomocí implicitní referenční nebo boxing převod. + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + Atribut RequiresUnsafeAttribute nelze pro tento symbol použít. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. Cílový modul runtime nepodporuje rozšířené typy rozložení. @@ -1857,11 +1902,6 @@ Očekávala se možnost warnings nebo annotations nebo konec direktivy. - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - Neplatná hodnota {0}: {1} pro jazyk C# {2}. Použijte prosím verzi jazyka {3} nebo vyšší. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. Pokud se nepoužívá verze jazyka {0} nebo novější, musí být pro parametr typu s možnou hodnotou null známo, že má typ hodnoty nebo typ odkazu, který není možné nastavit na null. Zvažte možnost změnit verzi jazyka nebo přidat class, struct nebo omezení typu. @@ -2677,6 +2717,21 @@ Neočekávaný seznam parametrů. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + Konstruktor deklarovaný v deklaraci union musí obsahovat inicializátor this, který volá syntetizovaný nebo explicitně deklarovaný konstruktor. + + + + A union declaration must specify at least one case type. + Deklarace typu union musí specifikovat aspoň jeden typ případu. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + Výraz typu {0} nelze zpracovat tímto vzorem; další chyby viz toto umístění. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. {0} má atribut UnmanagedCallersOnly a nedá se volat napřímo. Pro tuto metodu získejte ukazatel na funkci. @@ -2687,11 +2742,36 @@ {0} má atribut UnmanagedCallersOnly a nedá se převést na typ delegáta. Pro tuto metodu získejte ukazatel na funkci. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - {0} se definuje v modulu s nerozpoznanou verzí RefSafetyRulesAttribute, očekává se hodnota 11. + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + {0} je definováno v modulu s nerozpoznanou verzí {1}, očekává se {2}. + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + Nezabezpečený kontext je vyžadován pro konstruktor {0} označený jako „RequiresUnsafe“ nebo „extern“, aby se vyhovělo omezení „new()“ parametru typu „{1}“ v „{2}“ + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + {0} musí být použito v nebezpečném kontextu, protože je označeno jako RequiresUnsafe nebo extern. + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + {0} musí být použito v nebezpečném kontextu, protože obsahuje ukazatele ve svém podpisu. + + This operation may only be used in an unsafe context + Tuto operaci lze použít pouze v nebezpečném kontextu. + + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + Výraz stackalloc bez inicializátoru uvnitř SkipLocalsInit se dá použít jenom v nebezpečném kontextu. + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. UnscopedRefAttribute nelze použít pro implementaci rozhraní, protože implementovaný člen {0} nemá tento atribut. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + argumenty výrazu kolekce @@ -3077,6 +3157,16 @@ nevázané obecné typy v operátoru nameof + + unions + sjednocení + + + + updated memory safety rules + aktualizovaná pravidla zabezpečení paměti + + unsigned right shift nepodepsaný pravý posun @@ -3862,6 +3952,16 @@ Člen struktury vrací this nebo jiné členy instance pomocí odkazu + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + Atribut RequiresUnsafeAttribute je platný pouze v rámci aktualizovaných pravidel zabezpečení paměti. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + Atribut RequiresUnsafeAttribute je platný pouze v rámci aktualizovaných pravidel zabezpečení paměti. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. Návratová hodnota musí být jiná než null, protože parametr {0} není null. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4245,11 +4345,11 @@ tvar: -d) -langversion:? Zobrazí povolené hodnoty pro verzi jazyka -langversion:<string> Určuje verzi jazyka, například - nejnovější (nejnovější verze včetně podverzí), - default (stejné jako latest), - latestmajor (nejnovější verze, kromě podverze), - preview (nejnovější verze včetně funkcí v nepodporované verzi Preview), - nebo konkrétní verze jako 6 nebo 7.1. + `latest` (nejnovější verze včetně podverzí), + `default` (stejné jako `latest`), + `latestmajor` (nejnovější verze, kromě podverze), + `preview` (nejnovější verze včetně funkcí v nepodporované verzi Preview), + nebo konkrétní verze jako `6` nebo `7.1` -nullable[+|-] Zadejte možnost kontextu s možnou hodnotou null enable|disable. -nullable:{enable|disable|warnings|annotations} Zadejte možnost kontextu s možnou hodnotou null enable|disable|warnings|annotations. @@ -4275,7 +4375,7 @@ -baseaddress:<address> Základní adresa knihovny, která se má vytvořit -checksumalgorithm:<alg> Určuje algoritmus pro výpočet kontrolního součtu zdrojového souboru uloženého v souboru PDB. Podporované hodnoty: - SHA1 nebo SHA256 (výchozí). + SHA1, SHA256 (predvolené), SHA384 alebo SHA512. -codepage:<n> Určuje znakovou stránku, která má být použita při otevírání zdrojových souborů -utf8output Výstupní zprávy kompilátoru v kódování UTF-8 @@ -5855,6 +5955,16 @@ Parametr se nepřečetl. Nezapomněli jste ho použít k inicializaci vlastnosti s daným názvem? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Modifikátor „unsafe“ nemá v rámci aktuálních pravidel zabezpečení paměti žádný vliv. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Modifikátor „unsafe“ nemá v rámci aktuálních pravidel zabezpečení paměti žádný vliv. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute je platný jenom v C# 11 nebo novějším nebo při cílení na net7.0 nebo novější. @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - Částečné deklarace {0} musí být jen třídy, jen třídy záznamů, jen struktury, jen struktury záznamů nebo jen rozhraní. + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + Částečné deklarace {0} musí být jen třídy, jen třídy záznamů, jen struktury, jen sjednocení, jen struktury záznamů nebo jen rozhraní. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf index 5b62cf64279e..26d1f78b8159 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.de.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + Keine Überladung der Methode „{0}“ akzeptiert {1} Argumente des with(...)-Elements. @@ -387,6 +387,21 @@ Das AsyncMethodBuilder-Attribut ist für anonyme Methoden ohne expliziten Rückgabetyp unzulässig. + + Unsafe member '{0}' cannot implement safe member '{1}' + Das unsichere Mitglied „{0}“ kann das sichere Mitglied „{1}“ nicht implementieren + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + Das unsichere Mitglied „{0}“ kann das sichere Mitglied „{1}“ nicht implizit implementieren + + + + Unsafe member '{0}' cannot override safe member '{1}' + Das unsichere Mitglied „{0}“ kann das sichere Mitglied „{1}“ nicht überschreiben + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. „OverloadResolutionPriorityAttribute“ kann für dieses Element nicht verwendet werden. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + Die Argumente des with(...)-Elements dürfen nicht dynamisch sein. 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + Das with(...)-Element für eine schreibgeschützte Schnittstelle muss leer sein, falls es vorhanden ist. 'with(...)' element must be the first element - 'with(...)' element must be the first element + Das with(...)-Element muss das erste Element sein. 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + with(...)-Elemente werden für den Typ „{0}“ nicht unterstützt. @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - Es wurde keine zugängliche Methode vom Typ "{0}" mit der erwarteten Signatur gefunden: eine statische Methode mit einem einzelnen Parameter vom Typ "ReadOnlySpan<{1}>" und Rückgabetyp "{2}". + Es wurde keine zugängliche Methode vom Typ „{0}“ mit der erwarteten Signatur gefunden: eine statische Methode, deren letzter Parameter vom Typ „ReadOnlySpan<{1}>“ und Rückgabetyp „{2}“ ist. @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + Der Elementtyp dieser Sammlung darf keine Referenzstruktur oder ein Typparameter sein, der Referenzstrukturen zulässt. + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + Ungültiger Wert für "{0}": "{1}" für C# {2}. Verwenden Sie Sprachversion {3} oder höher. @@ -902,6 +922,11 @@ Eine Ausdrucksbaumstruktur darf keinen ==- oder !=-Tupeloperator enthalten. + + An expression tree may not contain a union conversion. + Ein Ausdrucksbaum darf keine Union-Konvertierung enthalten. + + An expression tree may not contain a with-expression. Eine Ausdrucksbaumstruktur darf keinen with-Ausdruck enthalten. @@ -1237,6 +1262,16 @@ Das Inlinearray-Elementfeld kann nicht als erforderlich, schreibgeschützt, flüchtig oder als Puffer fester Größe deklariert werden. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + Explizit deklarierte öffentliche Konstruktoren mit nur einem Parameter sind in einer „union“-Deklaration nicht erlaubt. + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + Instanzfelder, automatische Eigenschaften oder feldähnliche Ereignisse sind in einer „union“-Deklaration nicht erlaubt. + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter „{0}“: Instanzmitglieder können in einem Erweiterungsblock mit unbenanntem Empfängerparameter nicht deklariert werden. @@ -1467,6 +1502,16 @@ „{0}“ enthält keine Definition für „{1}“, und es konnte keine zugängliche Erweiterungsmethode „{1}“ gefunden werden, die ein erstes Argument vom Typ „{0}“ akzeptiert (wollten Sie die asynchrone Sammlung mit „await foreach“ durchlaufen?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + Der Typ „{0}“ kann nicht über eine implizite Referenz- oder Boxing-Konvertierung in "object" umgewandelt werden + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute kann nicht auf dieses Symbol angewendet werden. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. Die Zielruntime unterstützt keine erweiterten Layouttypen. @@ -1857,11 +1902,6 @@ Erwartet wurde "warnings", "annotations" oder das Ende der Anweisung. - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - Ungültiger Wert für "{0}": "{1}" für C# {2}. Verwenden Sie Sprachversion {3} oder höher. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. Ein Parameter mit Nullable-Typ muss als Werttyp oder Nicht-Nullable-Verweistyp bekannt sein, es sei denn, die Sprachversion "{0}" oder höher wird verwendet. Erwägen Sie, die Sprachversion zu ändern oder eine class-, struct- oder type-Einschränkung hinzuzufügen. @@ -2677,6 +2717,21 @@ Unerwartete Parameterliste. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + Ein in einer „union“-Deklaration deklarierter Konstruktor muss einen „this“-Initialisierer haben, der einen synthetisierten Konstruktor oder einen explizit deklarierten Konstruktor aufruft. + + + + A union declaration must specify at least one case type. + Eine „union“-Deklaration muss mindestens einen Anfragetyp angeben. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + Ein Ausdruck vom Typ „{0}“ kann von diesem Muster nicht verarbeitet werden. Weitere Fehler finden Sie an dieser Stelle. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. "{0}" ist mit dem Attribut "UnmanagedCallersOnly" versehen und kann nicht direkt aufgerufen werden. Rufen Sie einen Funktionszeiger auf diese Methode ab. @@ -2687,11 +2742,36 @@ "{0}" ist mit dem Attribut "UnmanagedCallersOnly" versehen und kann nicht in einen Delegattyp konvertiert werden. Rufen Sie einen Funktionszeiger auf diese Methode ab. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - „{0}“ ist in einem Modul mit einer unbekannten RefSafetyRulesAttribute-Version definiert, erwartet wird „11“. + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + „{0}“ ist in einem Modul mit einer unbekannten Version von {1} definiert, erwartet wird „{2}“. + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + Für den als 'RequiresUnsafe' oder 'extern' markierten Konstruktor '{0}' ist ein unsicherer Kontext erforderlich, um die 'new()'-Einschränkung des Typparameters '{1}' in '{2}' zu erfüllen. + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + „{0}“ muss in einem unsicheren Kontext verwendet werden, da er als „RequiresUnsafe“ oder „extern“ gekennzeichnet ist + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + „{0}“ muss in einem unsicheren Kontext verwendet werden, da die Signatur Zeiger enthält + + This operation may only be used in an unsafe context + Dieser Vorgang darf nur in einem unsicheren Kontext verwendet werden + + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + stackalloc-Ausdruck ohne Initialisierer in SkipLocalsInit darf nur in einem unsicheren Kontext verwendet werden + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. „UnscopedRefAttribute“ kann nicht auf eine Schnittstellenimplementierung angewendet werden, da der implementierte Member „{0}“ nicht über dieses Attribut verfügt. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + Argumente des Sammlungsausdrucks @@ -3077,6 +3157,16 @@ Ungebundene generische Typen im nameof-Operator + + unions + Unions + + + + updated memory safety rules + Aktualisierte Speichersicherheitsregeln + + unsigned right shift Unsignierte Rechtsverschiebung @@ -3862,6 +3952,16 @@ Der Strukturmember gibt "this" oder andere Instanzmember als Verweis zurück. + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute ist nur unter den aktualisierten Speichersicherheitsregeln gültig. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute ist nur unter den aktualisierten Speichersicherheitsregeln gültig. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. Der Rückgabewert muss ungleich NULL sein, weil der Parameter "{0}" nicht NULL ist. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4245,11 +4345,11 @@ (Kurzform: -d) -langversion:? Anzeigen der zulässigen Werte für die Sprachversion -langversion:<string> Sprachversion angeben, z. B. - „latest“ (neueste Version, einschließlich Nebenversionen), - „default“ (identisch mit „latest“), - „latestmajor“ (neueste Version, einschließlich Nebenversionen), - „preview“ (neueste Version, einschließlich Features in nicht unterstützter Vorschau), - oder bestimmte Versionen wie „6“ oder „7.1“ + `latest` (neueste Version, einschließlich Nebenversionen), + `default` (identisch mit `latest`), + `latestmajor` (neueste Version, einschließlich Nebenversionen), + `preview` (neueste Version, einschließlich Features in nicht unterstützter Vorschau), + oder bestimmte Versionen wie `6` oder `7.1` -nullable[+|-] Gibt die „Nullwerte zulassend“-Kontextoption enable|disable an. -nullable:{enable|disable|warnings|annotations} Gibt die „Nullwerte zulassend“-Kontextoption enable|disable|warnings|annotations an. @@ -4275,7 +4375,7 @@ -baseaddress:<address> Basisadresse für die zu erstellende Bibliothek -checksumalgorithm:<alg> Gibt einen Algorithmus zum Berechnen der Prüfsumme-Quelldatei an, die in PDB gespeichert ist. Folgende Werte werden unterstützt: - SHA1 oder SHA256 (Standard). + SHA-1, SHA-256 (Standard), SHA-384 oder SHA-512. -codepage:<n> Gibt die beim Öffnen von Quelldateien zu verwendende Codepage an -utf8output Ausgabecompilermeldungen in UTF-8-Codierung @@ -4300,7 +4400,7 @@ -lib:<file list> Gibt zusätzliche Verzeichnisse an, in denen nach Verweise gesucht werden soll -errorreport:<string> Gibt an, wie interne Compiler-Fehler behandelt werden sollen: - „prompt“, „send“, „queue“ oder „none“. Der Standardwert ist + "prompt", "send", "queue" oder "none". Der Standardwert ist „queue“. -appconfig:<file> Gibt eine Anwendungskonfigurationsdatei an, die Assemblybindungseinstellungen enthält @@ -5855,6 +5955,16 @@ Der Parameter wird nicht gelesen. Möglicherweise haben Sie ihn nicht zum Initialisieren der gleichnamigen Eigenschaft verwendet? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Der „unsafe“-Modifizierer hat hier unter den aktuellen Speichersicherheitsregeln keine Auswirkungen. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Der „unsafe“-Modifizierer hat hier unter den aktuellen Speichersicherheitsregeln keine Auswirkungen. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute ist nur in C# 11 oder höher oder bei net7.0 oder höher gültig. @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - Partielle Deklarationen von "{0}" müssen entweder nur Klassen, nur Datensatzklassen, nur Strukturen, nur Datensatzstrukturen oder nur Schnittstellen sein. + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + Partielle Deklarationen von „{0}“ müssen alle Klassen, alle Datensatzklassen, alle Strukturen, alle Unions, alle Datensatzstrukturen oder alle Schnittstellen sein. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf index e69904670d14..d53e1bbf32e8 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.es.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + Ninguna sobrecarga del método '{0}' acepta argumentos del elemento 'with(...)' {1} @@ -387,6 +387,21 @@ El atributo AsyncMethodBuilder no se permite en métodos anónimos sin un tipo de valor devuelto explícito. + + Unsafe member '{0}' cannot implement safe member '{1}' + El miembro no seguro ''{0}'' no puede implementar el miembro seguro '{1}' + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + El miembro no seguro ''{0}'' no puede implementar implícitamente el miembro seguro ''{1}'' + + + + Unsafe member '{0}' cannot override safe member '{1}' + El miembro no seguro ''{0}'' no puede invalidar el miembro seguro ''{1}'' + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. No se puede usar "OverloadResolutionPriorityAttribute" en este miembro. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + Los argumentos del elemento 'with(...)' no pueden ser dinámicos 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + El elemento 'with(...)' de una interfaz de solo lectura debe estar vacío si está presente 'with(...)' element must be the first element - 'with(...)' element must be the first element + El elemento 'with(...)' debe ser el primer elemento 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + No se admiten elementos 'with(...)' para el tipo '{0}' @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - No se encontró ningún método "{0}" accesible con la firma esperada: un método estático con un único parámetro de tipo "ReadOnlySpan<{1}>" y el tipo de valor devuelto "{2}". + No se pudo encontrar un método '{0}' accesible con la signatura esperada: un método estático cuyo último parámetro es de tipo 'ReadOnlySpan<{1}>' y tipo de valor devuelto '{2}'. @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + El tipo de elemento de esta colección no puede ser una estructura de referencia ni un parámetro de tipo que permita estructuras de referencia + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + Valor "{0}" no válido: "{1}" para C# {2}. Use la versión del lenguaje "{3}" o una posterior. @@ -902,6 +922,11 @@ Un árbol de expresión no puede contener un operador de tupla == o !=. + + An expression tree may not contain a union conversion. + Un árbol de expresión no puede contener una conversión de unión. + + An expression tree may not contain a with-expression. Un árbol de expresión no puede contener una expresión with. @@ -1237,6 +1262,16 @@ El campo de elemento de matriz insertada no se puede declarar como obligatorio, de solo lectura, volátil o como búfer de tamaño fijo. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + No se permiten constructores públicos declarados explícitamente con un solo parámetro en una declaración "union". + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + Los campos de instancia, las propiedades automáticas o los eventos de tipo campo no se permiten en una declaración "union". + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter “{0}”: no se pueden declarar miembros de instancia en un bloque de extensión con un parámetro receptor sin nombre @@ -1467,6 +1502,16 @@ '{0}' no contiene una definición para '{1}' y no se encontró ningún método de extensión accesible '{1}' aceptando un primer argumento de tipo '{0}' (¿pretendía iterar por la colección asincrónica con 'await foreach' en su lugar?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + No se puede convertir el tipo "{0}" en "object" a través de una referencia implícita o conversión boxing + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute no se puede aplicar a este símbolo. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. El runtime de destino no admite tipos con diseño extendido. @@ -1857,11 +1902,6 @@ Se esperaban "advertencias", "anotaciones" o el final de la directiva - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - Valor "{0}" no válido: "{1}" para C# {2}. Use la versión del lenguaje "{3}" o una posterior. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. Debe saberse si un parámetro de tipo que acepta valores NULL es un tipo de valor o un tipo de referencia que no acepta valores NULL, a menos que se use la versión de lenguaje "{0}" o una posterior. Considere la posibilidad de cambiar la versión de lenguaje o de agregar "class", "struct" o una restricción de tipo. @@ -2677,6 +2717,21 @@ Lista de parámetros inesperada. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + Un constructor declarado en una declaración de"unión" debe tener un inicializador "this" que llame a un constructor sintetizado o a un constructor declarado explícitamente. + + + + A union declaration must specify at least one case type. + Una declaración de unión debe especificar al menos un tipo de caso. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + Este patrón no puede controlar una expresión de tipo ''{0}", consulte los errores adicionales en esta ubicación. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. "{0}" tiene un atributo "UnmanagedCallersOnly" y no se le puede llamar directamente. Obtenga un puntero de función a este método. @@ -2687,11 +2742,36 @@ ' {0} ' tiene un atributo ' UnmanagedCallersOnly ' y no se puede convertir en un tipo de delegado. Obtenga un puntero de función a este método. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - '{0}' se define en un módulo con una versión refSafetyRulesAttribute no reconocida, que espera '11'. + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + ''{0}'' se define en un módulo con una versión no reconocida {1}, que espera ''{2}''. + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + Se requiere un contexto no seguro para que el constructor ''{0}'' marcado como ''RequiresUnsafe'' o 'extern' cumpla la restricción 'new()' del parámetro de tipo ''{1}'' en ''{2}'' + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + ''{0}'' debe usarse en un contexto no seguro porque está marcado como ''RequiresUnsafe'' o ''extern'' + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + ''{0}'' debe usarse en un contexto no seguro porque tiene punteros en su firma + + + + This operation may only be used in an unsafe context + Esta operación solo se puede usar en un contexto no seguro + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + La expresión stackalloc sin un inicializador dentro de SkipLocalsInit solo se puede usar en un contexto no seguro + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. UnscopedRefAttribute no se puede aplicar a una implementación de interfaz porque el miembro implementado "{0}" no tiene este atributo. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + argumentos de expresión de colección @@ -3077,6 +3157,16 @@ tipos genéricos no enlazados en el operador nameof + + unions + uniones + + + + updated memory safety rules + reglas de seguridad de memoria actualizadas + + unsigned right shift cambio derecho sin firmar @@ -3862,6 +3952,16 @@ El miembro de estructura devuelve "this" u otros miembros de instancia por referencia + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute solo es válido en las reglas de seguridad de memoria actualizadas. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute solo es válido en las reglas de seguridad de memoria actualizadas. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. El valor devuelto debe ser distinto de NULL porque el parámetro "{0}" no es NULL. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4170,40 +4270,40 @@ -t:appcontainerexe) -target:winmdobj Compilar un archivo intermedio de Windows Runtime que WinMDExp consume (forma corta: -t:winmdobj) --doc:<file> Archivo de documentación XML que se va a generar --refout:<file> Salida del ensamblado de referencia que se va a generar --platform:<string> Limitar en qué plataformas se puede ejecutar este código: x86, +-doc:<archivo> Archivo de documentación XML que se va a generar +-refout:<archivo> Salida del ensamblado de referencia que se va a generar +-platform:<cadena> Limitar en qué plataformas se puede ejecutar este código: x86, Itanium, x64, arm, arm64, anycpu32bitpreferred o anycpu. El valor predeterminado es anycpu. - ARCHIVOS DE ENTRADA - --recurse:<wildcard> Incluir todos los archivos del directorio y +-recurse:<carácter comodín> Incluir todos los archivos del directorio y y subdirectorios actuales según las especificaciones del carácter comodín --reference:<alias>=<file> Metadatos de referencia del archivo de ensamblado especificado +-reference:<alias>=<archivo> Metadatos de referencia del archivo de ensamblado especificado con el alias proporcionado (forma corta: -r) -/reference:<file list> Metadatos de referencia de los archivos de ensamblado +/reference:<lista de archivos> Metadatos de referencia de los archivos de ensamblado especificados (Forma corta: -r) --addmodule:<file list> Vincular los módulos especificados a este ensamblado --link:<file list> Insertar metadatos de los archivos de ensamblado de la interoperabilidad +-addmodule:<lista de archivos> Vincular los módulos especificados a este ensamblado +-link:<lista de archivos> Insertar metadatos de los archivos de ensamblado de la interoperabilidad especificada (Forma corta: -r) --analyzer:<file list> Ejecutar los analizadores desde este ensamblado +-analyzer:<lista de archivos> Ejecutar los analizadores desde este ensamblado (Forma corta: -a) --additionalfile:<file list> Archivos adicionales que no afectan directamente a la generación de +-additionalfile:<lista de archivos> Archivos adicionales que no afectan directamente a la generación de código, pero los analizadores pueden usarse para producir errores o advertencias. -embed Insertar todos los archivos de código fuente en el archivo PDB. --embed:<file list> Insertar archivos específicos en el archivo PDB. +-embed:<lista de archivos> Insertar archivos específicos en el archivo PDB. - RECURSOS - --win32res:<file> Especificar el archivo de recursos Win32 (.res) --win32icon:<file> Usar este icono para la salida --win32manifest:<file> Especificar un archivo de manifiesto win32 (.xml) +-win32res:<archivo> Especificar el archivo de recursos Win32 (.res) +-win32icon:<archivo> Usar este icono para la salida +-win32manifest:<archivo> Especificar un archivo de manifiesto win32 (.xml) -nowin32manifest No incluir el manifiesto Win32 predeterminado -resource:<resinfo> Insertar el recurso especificado (forma corta: /res) -linkresource:<resinfo> Vincular el recurso especificado a este ensamblado (Forma corta: -linkres) En la que el formato de resinfo - es <file>[,<string name>[,public|private]] + is <archivo>[,<nombre de cadena>[,public|private]] - GENERACIÓN DE CÓDIGO - -debug[+|-] Emitir información de depuración @@ -4218,18 +4318,18 @@ -refonly Generar un ensamblado de referencia en lugar de la salida principal -instrument:TestCoverage Producir un ensamblado instrumentado para recopilar información de cobertura --sourcelink:<file> Información del vínculo de origen para insertar en el archivo PDB. +-sourcelink:<archivo> Información del vínculo de origen para insertar en el archivo PDB. - ERRORES Y ADVERTENCIAS - -warnaserror[+|-] Notificar todas las advertencias como errores --warnaserror[+|-]:<warn list> Notificar advertencias específicas como errores +-warnaserror[+|-]:<lista de advertencias> Notificar advertencias específicas como errores (use "nullable" para todas las advertencias que acepten valores NULL) -warn:<n> Establecer el nivel de advertencia (0 o superior) (forma corta: -w) -nowarn:<warn list> Deshabilitar mensajes de advertencia específicos (use "nullable" para todas las advertencias que acepten valores NULL) --ruleset:<file> Especificar un archivo de conjunto de reglas que deshabilite +-ruleset:<archivo> Especificar un archivo de conjunto de reglas que deshabilite diagnósticos específicos. --errorlog:<file>[,version=<sarif_version>] +-errorlog:<archivo>[,version=<sarif_version>] Especifique un archivo para registrar todos los diagnósticos del compilador y el analizador. sarif_version:{1|2|2.1} El valor predeterminado es 1.2 y 2.1 @@ -4241,15 +4341,15 @@ - LENGUAJE - -checked[+|-] Generar comprobaciones de desbordamiento -unsafe[+|-] Permitir código no seguro --define:<symbol list> Definir los símbolos de la compilación condicional (forma +-define:<lista de símbolos> Definir los símbolos de la compilación condicional (forma corta: -d) -langversion:? Muestra los valores permitidos para la versión del lenguaje --langversion:<string> Especificar la versión de lenguaje como - "latest" (versión más reciente, incluidas las versiones secundarias), - "default" (igual que "más reciente"), - “latestmajor” (versión más reciente, incluidas las versiones secundarias), - "preview" (versión más reciente, incluidas las características de la versión preliminar no admitida) - o versiones específicas, como "6" o "7.1" +-langversion:<cadena> Especificar la versión de lenguaje como + `latest` (versión más reciente, incluidas las versiones secundarias), + `default` (igual que `latest`), + `latestmajor` (versión más reciente, incluidas las versiones secundarias), + `preview` (versión más reciente, incluidas las características de la versión preliminar no admitida) + o versiones específicas, como `6` o `7.1` -nullable[+|-] Especificar la opción de contexto que acepta valores NULL: habilitar|deshabilitar. -nullable:{enable|disable|warnings|annotations} Especifique la opción de contexto que acepta valores NULL: enable|disable|warnings|annotations. @@ -4259,12 +4359,12 @@ de la clave de nombre seguro -publicsign[+|-] Firmar públicamente el ensamblado usando solo la parte pública de la clave de nombre seguro --keyfile:<file> Especificar un archivo de clave de nombre seguro --keycontainer:<string> Especificar un contenedor de claves de nombre seguro +-keyfile:<archivo> Especificar un archivo de clave de nombre seguro +-keycontainer:<cadena> Especificar un contenedor de claves de nombre seguro -highentropyva[+|-] Habilitar ASLR de alta entropía - VARIOS - -@<file> Leer el archivo de respuesta para obtener más opciones +@<archivo> Leer el archivo de respuesta para obtener más opciones -help Mostrar este mensaje de uso (forma corta: -?) -nologo Suprimir el mensaje de copyright del compilador -noconfig No incluir automáticamente el archivo CSC.RSP @@ -4272,14 +4372,14 @@ -version Mostrar el número de versión del compilador y salir. - AVANZADO - --baseaddress:<address> Dirección base para la biblioteca que se compilará --checksumalgorithm:<alg> Especificar el algoritmo para calcular la suma de comprobación +-baseaddress:<dirección> Dirección base para la biblioteca que se compilará +-checksumalgorithm:<alg.> Especificar el algoritmo para calcular la suma de comprobación del archivo de origen almacenada en PDB. Los valores admitidos son: - SHA1 o SHA256 (valor predeterminado). + SHA1, SHA256 (predeterminado), SHA384 o SHA512. -codepage:<n> Especificar la página de códigos que se va a utilizar cuando se abren los archivos de código fuente -utf8output Salida de mensajes del compilador en codificación UTF-8 --main:<type> Especificar el tipo que contiene el punto de entrada +-main:<tipo> Especificar el tipo que contiene el punto de entrada (omitir todos los demás puntos de entrada posibles) (forma corta: -m) -fullpaths El compilador genera rutas de acceso completas @@ -4288,25 +4388,25 @@ -pathmap:<K1>=<V1>,<K2>=<V2>,... Especifique una asignación para los nombres de ruta de acceso de origen generados por el compilador. --pdb:<file> Especificar el nombre del archivo de información de depuración (predeterminado: +-pdb:<archivo> Especificar el nombre del archivo de información de depuración (predeterminado: nombre de archivo de salida con extensión .pdb) -errorendlocation Línea de salida y columna de la ubicación final de cada error -preferreduilang Especificar el nombre del idioma de salida preferido. -nosdkpath Deshabilitar la búsqueda de rutas de acceso SDK predeterminadas para los ensamblados de biblioteca estándar. --sdkpath:<path> Ruta de acceso usada para buscar ensamblados de biblioteca estándar. +-sdkpath:<ruta> Ruta de acceso usada para buscar ensamblados de biblioteca estándar. -nostdlib No hacer referencia a bibliotecas estándar (mscorlib.dll) --subsystemversion:<string> Especificar versión del subsistema de este ensamblado --lib:<file list> Especificar directorios adicionales en los que buscar +-subsystemversion:<cadena> Especificar versión del subsistema de este ensamblado +-lib:<lista de archivos> Especificar directorios adicionales en los que buscar referencias --errorreport:<string> Especificar cómo controlar los errores del compilador interno: +-errorreport:<cadena> Especificar cómo controlar los errores del compilador interno: solicitar, enviar, poner en cola o ninguno. El valor predeterminado es en cola. --appconfig:<file> Especificar un archivo de configuración de la aplicación +-appconfig:<archivo> Especificar un archivo de configuración de la aplicación que contiene la configuración de enlace del ensamblado --moduleassemblyname:<string> Nombre del ensamblado del que formará parte +-moduleassemblyname:<cadena> Nombre del ensamblado del que formará parte este módulo --modulename:<string> Especificar el nombre del módulo de origen +-modulename:<cadena> Especificar el nombre del módulo de origen -generatedfilesout:<dir> Colocar archivos generados durante la compilación en el directorio especificado. -reportivts[+|-] Información de salida sobre todos los IVT concedidos a este @@ -5855,6 +5955,16 @@ El parámetro no se ha leído ¿Olvidó usarlo para inicializar la propiedad con ese nombre? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + El modificador "unsafe" no tiene ningún efecto aquí en las reglas de seguridad de memoria actuales. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + El modificador "unsafe" no tiene ningún efecto aquí en las reglas de seguridad de memoria actuales. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute solo es válido en C# 11 o posterior o cuando el destino es net7.0 o posterior. @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - Las declaraciones parciales de '{0}' deben ser todas las clases, todas las clases de registro, todos los registros, todos los registros o todas las interfaces + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + Las declaraciones parciales de "{0}" deben ser todas las clases, todas las clases de registro, todas las estructuras, todas las uniones, todas las estructuras de registro o todas las interfaces diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf index 56836d8c4854..e2aad22ad077 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.fr.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + Aucune surcharge de la méthode « {0} » n’accepte les arguments d’élément « with(...) » {1} @@ -387,6 +387,21 @@ L'attribut AsyncMethodBuilder n'est pas autorisé pour les méthodes anonymes sans type de retour explicite. + + Unsafe member '{0}' cannot implement safe member '{1}' + Le membre non sécurisé « {0} » ne peut pas implémenter le membre sécurisé « {1} » + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + Un membre non sécurisé « {0} » ne peut pas implémenter implicitement un membre sécurisé « {1} » + + + + Unsafe member '{0}' cannot override safe member '{1}' + Le membre non sécurisé « {0} » ne peut pas remplacer le membre approuvé « {1} » + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. Impossible d'utiliser « OverloadResolutionPriorityAttribute » sur ce membre. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + Les arguments de l’élément « with(...) » ne peuvent pas être dynamiques 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + L’élément « with(...) » pour une interface en lecture seule doit être vide s’il est présent 'with(...)' element must be the first element - 'with(...)' element must be the first element + L’élément « with(...) » doit être le premier élément 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + Les éléments « with(...) » ne sont pas pris en charge pour le type « {0} » @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - Désolé... Nous n’avons pas pou trouver une méthode « {0} » accessible avec la signature attendue : une méthode statique avec un seul paramètre de type « ReadOnlySpan<{1}> » et le type de retour « {2} ». + Désolé... Nous n’avons pas pou trouver une méthode « {0} » accessible avec la signature attendue : une méthode statique dont le dernier paramètre est de type « ReadOnlySpan<{1}> » et le type de retour « {2} ». @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + Le type d’élément de cette collection ne peut pas être un struct ref ni un paramètre de type autorisant les structs ref + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + Valeur '{0}' non valide : '{1}' pour C# {2}. Utilisez la version de langage '{3}' ou une version ultérieure. @@ -902,6 +922,11 @@ Une arborescence de l'expression ne peut pas contenir un opérateur de tuple == ou != + + An expression tree may not contain a union conversion. + Un arbre d'expression ne peut pas contenir de conversion d'union. + + An expression tree may not contain a with-expression. Une arborescence de l'expression ne peut pas contenir d'expression with. @@ -1237,6 +1262,16 @@ Le champ d’élément de tableau inlined ne peut pas être déclaré comme obligatoire, en lecture seule, volatile ou en tant que mémoire tampon de taille fixe. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + Les constructeurs publics explicitement déclarés avec un seul paramètre ne sont pas autorisés dans une déclaration « union ». + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + Les champs d'instance, les propriétés automatiques ou les événements de type champ ne sont pas autorisés dans une déclaration « union ». + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter « {0} » : impossible de déclarer des membres d’instance dans un bloc d’extension avec un paramètre de récepteur non nommé @@ -1467,6 +1502,16 @@ « {0} » ne contient pas de définition pour « {1} » et aucune méthode d’extension accessible « {1} » acceptant un premier argument de type « {0} » n’a pu être trouvée (vouliez-vous plutôt itérer la collection asynchrone avec « await foreach » ?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + Impossible de convertir le type '{0}' en 'objet' via une référence implicite ou une conversion d'encapsulation + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute ne peut pas être appliqué à ce symbole. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. Le runtime cible ne prend pas en charge les types de disposition étendus. @@ -1857,11 +1902,6 @@ 'warnings', 'annotations' ou fin de directive attendu - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - Valeur '{0}' non valide : '{1}' pour C# {2}. Utilisez la version de langage '{3}' ou une version ultérieure. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. Un paramètre de type nullable doit être connu pour pouvoir être un type valeur ou un type référence non-nullable, sauf si le langage version '{0}' ou ultérieure est utilisé. Pensez à changer la version du langage ou à ajouter une contrainte 'class', 'struct' ou de type. @@ -2677,6 +2717,21 @@ Liste de paramètres inattendue. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + Un constructeur déclaré dans une déclaration « union » doit avoir « ce » initialiseur qui appelle un constructeur synthétisé ou un constructeur déclaré explicitement. + + + + A union declaration must specify at least one case type. + Une déclaration d'union doit spécifier au moins un type de cas. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + Ce modèle ne peut pas gérer une expression de type '{0}', voir les erreurs supplémentaires à cet emplacement. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. '{0}' est attribué avec 'UnmanagedCallersOnly' et ne peut pas être appelé directement. Obtenez un pointeur de fonction vers cette méthode. @@ -2687,11 +2742,36 @@ '{0}' est attribué avec 'UnmanagedCallersOnly' et ne peut pas être converti en type délégué. Obtenez un pointeur de fonction vers cette méthode. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - '{0}' est défini dans un module avec une version RefSafetyRulesAttribute non reconnue, '11' attendu. + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + « {0} » est défini dans un module dont la version n’est pas reconnue {1} et attend « {2} ». + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + Un contexte non sécurisé est requis pour le constructeur « {0} » marqué comme « RequiresUnsafe » ou « extern » afin de satisfaire la contrainte « new() » du paramètre de type « {1} » dans « {2} » + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + « {0} » doit être utilisé dans un contexte non sécurisé, car il est marqué comme « RequiresUnsafe » ou « extern » + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + « {0} » doit être utilisé dans un contexte non sécurisé, car sa signature contient des pointeurs + + This operation may only be used in an unsafe context + Cette opération ne peut être utilisée que dans un contexte non sécurisé + + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + Une expression stackalloc sans initialiseur dans SkipLocalsInit ne peut être utilisée que dans un contexte non sécurisé + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. UnscopedRefAttribute ne peut pas être appliqué à une implémentation d’interface, car le membre implémenté « {0} » n’a pas cet attribut. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + arguments d’expression de collection @@ -3077,6 +3157,16 @@ types génériques indépendants dans l’opérateur nameof + + unions + syndicats + + + + updated memory safety rules + règles mises à jour de sécurité de la mémoire + + unsigned right shift shift droit non signé @@ -3862,6 +3952,16 @@ Le membre struct retourne 'this' ou d'autres membres d'instance par référence + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute est valide uniquement dans le cadre des règles de sécurité mémoire mises à jour. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute est valide uniquement dans le cadre des règles de sécurité mémoire mises à jour. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. La valeur de retour doit être non null, car le paramètre '{0}' a une valeur non null. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4265,7 +4365,7 @@ - DIVERS - @<file> Lire le fichier de réponse pour plus d’options --help Afficher ce message d’utilisation (forme courte : - ?) +-help Afficher ce message d’utilisation (forme courte : -?) -nologo Supprimer le message de droits d’auteur du compilateur -noconfig Ne pas inclure automatiquement le fichier CSC.RSP -parallel[+|-] Build simultané. @@ -4275,7 +4375,7 @@ -baseaddress:<address> Adresse de base de la bibliothèque à générer -checksumalgorithm:<alg> Spécifier l’algorithme pour calculer la somme de contrôle du fichier source stocké dans la PDB. Les valeurs prises en charge sont : - SHA1 ou SHA256 (par défaut). + SHA1, SHA256 (par défaut), SHA384, ou SHA512. -codepage:<n> Spécifier la page de code à utiliser lors de l’ouverture des fichiers source -utf8output Sortir les messages du compilateur au codage UTF-8 @@ -4300,7 +4400,7 @@ -lib:<file list> Spécifier des répertoires supplémentaires dans lesquels rechercher des références -errorreport:<string> Spécifier comment gérer les erreurs internes du compilateur : - prompt, send, queue, ou none. La valeur par défaut est + invite, envoie, file d’attente ou aucun. La valeur par défaut est file d’attente. -appconfig:<file> Spécifier un fichier de configuration de l’application contenant les paramètres de liaison d’assembly @@ -5855,6 +5955,16 @@ Le paramètre est non lu. Avez-vous oublié de l'utiliser pour initialiser la propriété portant ce nom ? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Le modificateur « unsafe » n’a aucun effet ici selon les règles actuelles de sécurité de la mémoire. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Le modificateur « unsafe » n’a aucun effet ici selon les règles actuelles de sécurité de la mémoire. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute n’est valide qu’en C# 11 ou version ultérieure, ou quand net7.0 ou version ultérieure est ciblé. @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - Les déclarations partielles de '{0}' doivent être toutes les classes, toutes les classes d’enregistrement, tous les structs, tous les structs d’enregistrement ou toutes les interfaces + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + Les déclarations partielles de '{0}' doivent être toutes les classes, toutes les classes d'enregistrements, toutes les structures, toutes les unions, toutes les structures d'enregistrements ou toutes les interfaces diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf index 992b81c098d5..ff736201a4df 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.it.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + Nessun overload per il metodo "{0}" accetta {1} con argomenti dell'elemento "with(...)" @@ -387,6 +387,21 @@ L'attributo AsyncMethodBuilder non è consentito in metodi anonimi senza un tipo restituito esplicito. + + Unsafe member '{0}' cannot implement safe member '{1}' + Il membro non sicuro '{0}' non può implementare il membro sicuro '{1}' + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + Il membro non sicuro '{0}' non può implementare in modo implicito il membro sicuro '{1}' + + + + Unsafe member '{0}' cannot override safe member '{1}' + Il membro non sicuro '{0}' non può eseguire l'override del membro sicuro '{1}' + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. Non è possibile usare 'OverloadResolutionPriorityAttribute' per questo membro. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + Gli argomenti dell'elemento "with(...)" non possono essere dinamici 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + L'elemento "with(...)" per un'interfaccia di sola lettura deve essere vuoto, se presente 'with(...)' element must be the first element - 'with(...)' element must be the first element + L'elemento "with(...)" deve essere il primo elemento 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + Gli elementi "with(...)" non sono supportati per il tipo "{0}" @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - Non è stato possibile trovare un metodo accessibile '{0}' con la firma prevista: un metodo statico con un singolo parametro di tipo 'ReadOnlySpan<{1}>' e di tipo restituito '{2}'. + Non è stato possibile trovare un metodo "{0}" accessibile con la firma prevista: un metodo statico di cui l'ultimo parametro sia di tipo "ReadOnlySpan<{1}>" e il tipo restituito sia "{2}". @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + Il tipo di elemento di questa raccolta non può essere uno struct di riferimento né un parametro di tipo che consente struct di riferimento + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + Valore '{1}' di '{0}' non valido per C# {2}. Usare la versione {3} o versioni successive del linguaggio. @@ -902,6 +922,11 @@ Un albero delle espressioni non può contenere un operatore == o != di tupla + + An expression tree may not contain a union conversion. + Un albero delle espressioni non può contenere una conversione 'union'. + + An expression tree may not contain a with-expression. Un albero delle espressioni non può contenere un'espressione with. @@ -1237,6 +1262,16 @@ Il campo dell'elemento della matrice inline non può essere dichiarato come obbligatorio, di sola lettura, volatile o come buffer a dimensione fissa. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + I costruttori pubblici dichiarati in modo esplicito con un singolo parametro non sono consentiti in una dichiarazione 'union'. + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + Campi di istanza, proprietà automatiche o eventi simili a campi non sono consentiti in una dichiarazione 'union'. + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter '{0}': non è possibile dichiarare i membri di istanza in un blocco di estensione con un parametro ricevitore senza nome @@ -1467,6 +1502,16 @@ '{0}' non contiene una definizione per '{1}' e non è possibile trovare alcun metodo di estensione accessibile '{1}' che accetta un primo argomento di tipo '{0}' (si intendeva forse eseguire l'iterazione sulla raccolta asincrona con 'await foreach'?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + Non è possibile convertire il tipo '{0}' in 'object' tramite un riferimento implicito o una conversione boxing + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + Non è possibile applicare RequiresUnsafeAttribute a questo simbolo. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. Il runtime di destinazione non supporta tipi di layout estesi. @@ -1857,11 +1902,6 @@ È previsto 'warnings', 'annotations' o la fine della direttiva - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - Valore '{1}' di '{0}' non valido per C# {2}. Usare la versione {3} o versioni successive del linguaggio. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. Un parametro di tipo nullable deve essere noto per essere un tipo valore o un tipo riferimento non nullable, a meno che non venga usata la versione '{0}' o successiva del linguaggio. Provare a cambiare la versione del linguaggio o ad aggiungere un vincolo di tipo, 'class' o 'struct'. @@ -2677,6 +2717,21 @@ Elenco parametri imprevisto. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + Un costruttore dichiarato in una dichiarazione 'union' deve avere un inizializzatore 'this' che chiama un costruttore sintetizzato o un costruttore dichiarato in modo esplicito. + + + + A union declaration must specify at least one case type. + Una dichiarazione union deve specificare almeno un tipo di caso. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + Un'espressione di tipo '{0}' non può essere gestita da questo criterio; vedere altri errori in questa posizione. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. '{0}', a cui è assegnato l'attributo 'UnmanagedCallersOnly', non può essere chiamato direttamente. Ottenere un puntatore a funzione per questo metodo. @@ -2687,11 +2742,36 @@ '{0}', a cui è assegnato l'attributo 'UnmanagedCallersOnly', non può essere convertito in un tipo delegato. Ottenere un puntatore a funzione per questo metodo. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - '{0}' è definito in un modulo con una versione non riconosciuta di RefSafetyRulesAttribute. È previsto '11'. + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + '{0}' è definito in un modulo con una versione non riconosciuta di {1}. È previsto '{2}'. + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + Per il costruttore '{0}' contrassegnato come 'RequiresUnsafe' o 'extern' è necessario un contesto unsafe per soddisfare il vincolo 'new()' del parametro di tipo '{1}' in '{2}' + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + '{0}' deve essere usato in un contesto non sicuro perché è contrassegnato come 'RequiresUnsafe' o 'extern' + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + '{0}' deve essere usato in un contesto non sicuro perché contiene puntatori nella firma + + This operation may only be used in an unsafe context + Questa operazione può essere usata solo in un contesto non sicuro + + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + l'espressione stackalloc senza un inizializzatore all'interno di SkipLocalsInit può essere usata solo in un contesto non sicuro + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. Non è possibile applicare UnscopedRefAttribute a un'implementazione dell'interfaccia perché il membro implementato '{0}' non dispone di questo attributo. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + argomenti dell'espressione di raccolta @@ -3077,6 +3157,16 @@ tipi generici non collegati nell'operatore nameof + + unions + union + + + + updated memory safety rules + regole di sicurezza della memoria aggiornate + + unsigned right shift spostamento a destra senza segno @@ -3862,6 +3952,16 @@ Il membro struct restituisce 'questo' o altri membri di istanza per riferimento + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute è valido solo con regole di sicurezza della memoria aggiornate. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute è valido solo con regole di sicurezza della memoria aggiornate. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. Il valore restituito deve essere non Null perché il parametro '{0}' è non Null. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4275,7 +4375,7 @@ target:module Compila un modulo che può essere aggiunto ad altro -baseaddress:<address> Indirizzo di base della libreria da compilare -checksumalgorithm:<alg> Consente di specificare l'algoritmo per calcolare il checksum del file di origine archiviato nel file PDB. I valori supportati sono: - SHA1 o SHA256 (impostazione predefinita). + SHA1, SHA256 (valore predefinito), SHA384 e SHA512. -codepage:<n> Specifica la tabella codici da utilizzare per l'apertura dei file di origine -utf8output Messaggi del compilatore di output nella codifica UTF-8 @@ -5855,6 +5955,16 @@ target:module Compila un modulo che può essere aggiunto ad altro Il parametro non è stato letto. Si è dimenticato di usarlo per inizializzare la proprietà con tale nome? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Il modificatore "unsafe" non ha alcun effetto qui secondo le regole correnti di sicurezza della memoria. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Il modificatore "unsafe" non ha alcun effetto qui secondo le regole correnti di sicurezza della memoria. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute è valido solo in C# 11 o versioni successive o se destinato a net7.0 o versione successiva. @@ -7131,8 +7241,8 @@ target:module Compila un modulo che può essere aggiunto ad altro - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - Le dichiarazioni parziali di '{0}' devono essere costituite solo da classi, classi di record, struct di record o interfacce + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + Le dichiarazioni parziali di '{0}' devono essere costituite solo da classi, classi di record, struct, unioni, struct di record o interfacce diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf index 1239698076f6..041f1f23555a 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ja.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + メソッド ‘{1}’ に、'with(...)' 要素の引数として {0} を受け取るオーバーロードはありません @@ -387,6 +387,21 @@ AsyncMethodBuilder 属性は、明示的な戻り値の型のない匿名メソッドでは許可されていません。 + + Unsafe member '{0}' cannot implement safe member '{1}' + 安全でないメンバー '{0}' は安全なメンバー '{1}' を実装できません + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + 安全でないメンバー '{0}' は、安全なメンバー '{1}' を暗黙的に実装できません + + + + Unsafe member '{0}' cannot override safe member '{1}' + 安全でないメンバー '{0}' はセーフ メンバー '{1}' をオーバーライドできません + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. このメンバーでは 'OverloadResolutionPriorityAttribute' を使用できません。 @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + 'with(...)' 要素の引数は動的にできません 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + 読み取り専用インターフェイスでは、'with(...)' 要素が存在する場合は空でなければなりません 'with(...)' element must be the first element - 'with(...)' element must be the first element + 'with(...)' 要素は最初の要素である必要があります 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + 'with(...)' 要素は型 '{0}' ではサポートされていません @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - 必要なシグネチャを持つアクセス可能な '{0}' メソッドが見つかりませんでした: 'ReadOnlySpan<{1}>' 型の単一のパラメーターと戻り値の型 '{2}' を持つ静的メソッド。 + 必要なシグネチャを持つアクセス可能な '{0}' メソッドが見つかりませんでした: 最後のパラメーターが 'ReadOnlySpan<{1}>' 型で戻り値の型が '{2}' の静的メソッド。 @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + このコレクションの要素型に、ref 構造体または ref 構造体を許可する型パラメーターは使用できません + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + '{0}' 値が無効です: C# {2} に対する '{1}'。言語バージョン '{3}' 以上をご使用ください。 @@ -902,6 +922,11 @@ 式ツリーにタプルの == または != 演算子を含めることはできません + + An expression tree may not contain a union conversion. + 式のツリーは、共用体変換を含むことはできません。 + + An expression tree may not contain a with-expression. 式ツリーは、with 式を含むことはできません @@ -1237,6 +1262,16 @@ インライン配列要素フィールドは、必須バッファー、読み取り専用バッファー、揮発性バッファー、または固定サイズ バッファーとして宣言できません。 + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + 単一のパラメーターを持つ明示的に宣言されたパブリック コンストラクターは、'union' 宣言では許可されていません。 + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + インスタンス フィールド、自動プロパティ、またはフィールドに似たイベントは、'union' 宣言では許可されません。 + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter '{0}': 名前のないレシーバー パラメーターを持つ拡張ブロックでインスタンス メンバーを宣言することはできません @@ -1467,6 +1502,16 @@ '{0}' に '{1}' の定義が含まれていません。また、型 '{0}' の最初の引数を受け入れるアクセス可能な拡張メソッド '{1}' が見つかりませんでした (代わりに 'await foreach' を使用して非同期コレクションを反復処理するつもりでしたか?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + 暗黙的な参照またはボックス化変換を使用して型 '{0}' を 'object' に変換することはできません + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute をこのシンボルに適用することはできません。 + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. ターゲット ランタイムは、拡張レイアウトの種類をサポートしていません。 @@ -1857,11 +1902,6 @@ 'warnings'、'annotations'、またはディレクティブの終わりが必要です - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - '{0}' 値が無効です: C# {2} に対する '{1}'。言語バージョン '{3}' 以上をご使用ください。 - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. 言語バージョン '{0}' 以上を使用していない限り、null 許容の型パラメーターは値の型または null 非許容の参照型であることがわかっている必要があります。言語バージョンを変更するか、'class'、'struct'、または型制約を追加することをご検討ください。 @@ -2677,6 +2717,21 @@ 予期しないパラメーター リストです。 + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + 'union' 宣言で宣言されたコンストラクターには、合成コンストラクターまたは明示的に宣言されたコンストラクターを呼び出す 'this' 初期化子が必要です。 + + + + A union declaration must specify at least one case type. + 共用体宣言では、少なくとも 1 つのケース型を指定する必要があります。 + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + 型 '{0}' の式は、このパターンでは処理できません。この場所で他のエラーを参照してください。 + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. '{0}' は 'UnmanagedCallersOnly' 属性が設定されているため、直接呼び出すことはできません。このメソッドへの関数ポインターを取得してください。 @@ -2687,11 +2742,36 @@ '{0}' は 'UnmanagedCallersOnly' 属性が設定されているため、デリゲート型に変換できません。このメソッドへの関数ポインターを取得してください。 UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - '{0}' は、認識されない RefSafetyRulesAttribute バージョンを持つモジュールで定義され、'11' が必要です。 + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + '{0}' は、認識されない {1} バージョンのモジュールで定義されています。'{2}' が予想されます。 + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + '{2}' の型パラメーター '{1}' の 'new()' 制約を満たすには、'requiresUnsafe' または 'extern' としてマークされたコンストラクター '{0}' に unsafe コンテキストが必要です + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + 'requiresUnsafe' または 'extern' としてマークされているため、'{0}' は安全でないコンテキストで使用する必要があります + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + '{0}' はシグネチャにポインターがあるため、安全でないコンテキストで使用する必要があります + + + + This operation may only be used in an unsafe context + この操作は、安全でないコンテキストでのみ使用できます + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + SkipLocalsInit 内の初期化子のない stackalloc 式は、安全でないコンテキストでのみ使用できます + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. 実装されたメンバー '{0}' には UnscopedRefAttribute が含まれていないため、この属性をインターフェイスの実装に適用することはできません。 @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + コレクション式の引数 @@ -3077,6 +3157,16 @@ nameof 演算子でバインドされていないジェネリック型 + + unions + 共用体 + + + + updated memory safety rules + 更新されたメモリの安全性ルール + + unsigned right shift 符号なし右シフト @@ -3862,6 +3952,16 @@ 構造体メンバーは 'this' または他のインスタンス メンバーを参照渡しで返します + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute は、更新されたメモリの安全性規則でのみ有効です。 + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute は、更新されたメモリの安全性規則でのみ有効です。 + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. パラメーター '{0}' が null 以外であるため、戻り値は null 以外でなければなりません。 @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4159,17 +4259,17 @@ - 出力ファイル - -out:<file> 出力ファイル名を指定します (既定値: main クラスを含むファイルの ベース名また最初のファイル) --target:exe コンソール アプリケーションをビルドします (既定) (短い +-target:exe コンソール アプリケーションを構築します (既定) (短い 形式: -t:exe) --target:winexe Windows 実行可能ファイルをビルドします (短い形式: +-target:winexe Windows 実行可能ファイルを構築します (短い形式: -t:winexe) --target:library ライブラリをビルドします (短い形式: -t:library) +-target:library ライブラリを構築します (短い形式: -t:library) -target:module 別のアセンブリに追加できるモジュールを - ビルドします (短い形式: -t:module) --target:appcontainerexe Appcontainer 実行可能ファイルをビルドします (短い形式: + 構築します (短い形式: -t:module) +-target:appcontainerexe Appcontainer 実行可能ファイルを構築します (短い形式: -t:appcontainerexe) -target:winmdobj WinMDExp が使用する、Windows ランタイムの - 中間ファイルをビルドします (短い形式: -t:winmdobj) + 中間ファイルを構築します (短い形式: -t:winmdobj) -doc:<file> 生成する XML ドキュメント ファイルです -refout:<file> 生成する参照アセンブリ出力です -platform:<string> このコードを実行できるプラットフォームを限定します。x86、 @@ -4206,7 +4306,7 @@ <file>[,<string name>[,public|private]] となります - コード生成 - --debug[+|-] デバッグ情報を出力します +-debug[+|-] デバッグ情報を生成します -debug:{full|pdbonly|portable|embedded} デバッグの種類を指定します ('full' は既定、 'portable' はクロスプラットフォーム形式、 @@ -4256,7 +4356,7 @@ - セキュリティ - -delaysign[+|-] 厳密な名前キーの公開部分のみを使用して - アセンブリに公開署名します + アセンブリに遅延署名します -publicsign[+|-] 厳密な名前キーの公開部分のみを使用して アセンブリに公開署名します -keyfile:<file> 厳密な名前のキー ファイルを指定します @@ -4268,14 +4368,14 @@ -help この使用法メッセージを表示します (短い形式: -?) -nologo コンパイラの著作権メッセージを表示しません -noconfig CSC.RSP ファイルを自動的に含めません --parallel[+|-] 同時にビルドします。 +-parallel[+|-] 同時に構築します。 -version コンパイラのバージョン番号を表示して終了します。 - 詳細設定 - --baseaddress:<address> ビルドするライブラリのベース アドレス +-baseaddress:<address> 構築するライブラリのベース アドレス -checksumalgorithm:<alg> PDB に格納されているソース ファイル チェックサムを計算するための アルゴリズムを指定します。サポートされている値: - SHA1 または SHA256 (既定値)。 + SHA1、SHA256 (既定)、SHA384、または SHA512。 -codepage:<n> ソース ファイルを開くときに使用するコードページを 指定します -utf8output UTF-8 エンコードでコンパイラのメッセージを出力します @@ -5855,6 +5955,16 @@ パラメーターが未読のため、この名前のプロパティを初期化するために使用していることを確認する必要がある + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 現在のメモリ安全性規則においては、'unsafe' 修飾子による影響はありません。 + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 現在のメモリ安全性規則においては、'unsafe' 修飾子による影響はありません。 + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute は、C# 11 以降、または net7.0 以降をターゲットにする場合にのみ有効です。 @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - '{0}' の partial 宣言は、すべてのクラス、すべてのレコード クラス、すべての構造体、すべてのレコード構造体、すべてのインターフェイスのいずれかにする必要があります + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + '{0}' の部分宣言は、すべてのクラス、すべてのレコード クラス、すべての構造体、すべての共用体、すべてのレコード構造体、またはすべてのインターフェイスである必要があります diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf index f274d2136c57..a4f5c43a6b71 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ko.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + {1} 'with(...)' 요소 인수를 사용하는 메서드 '{0}'에 대한 오버로드가 없습니다. @@ -387,6 +387,21 @@ AsyncMethodBuilder 특성은 명시적 반환 형식이 없는 익명 메서드에서 허용되지 않습니다. + + Unsafe member '{0}' cannot implement safe member '{1}' + 안전하지 않은 멤버 '{0}'은(는) 안전한 멤버 '{1}'을(를) 구현할 수 없습니다. + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + 안전하지 않은 멤버 '{0}'은(는) 안전한 멤버 '{1}'을(를) 암시적으로 구현할 수 없습니다. + + + + Unsafe member '{0}' cannot override safe member '{1}' + 안전하지 않은 멤버 '{0}'은(는) 안전한 멤버 '{1}'을(를) 재정의할 수 없습니다. + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. 이 멤버에 'OverloadResolutionPriorityAttribute'를 사용할 수 없습니다. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + 'with(...)' 요소 인수는 동적일 수 없습니다. 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + 읽기 전용 인터페이스의 'with(...)' 요소는 비어 있어야 합니다(있는 경우). 'with(...)' element must be the first element - 'with(...)' element must be the first element + 'with(...)' 요소는 첫 번째 요소여야 합니다. 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + 'with(...)' 요소는 '{0}' 형식에 대해 지원되지 않습니다. @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - 필요한 시그니처가 있는 액세스 가능한 '{0}' 메서드(형식이 'ReadOnlySpan<{1}>'인 단일 매개 변수가 및 '{2}' 반환 형식이 있는 정적 메서드)를 찾을 수 없습니다. + 필요한 시그니처가 있는 액세스 가능한 '{0}' 메서드(마지막 매개 변수가 'ReadOnlySpan<{1}>' 형식이고 반환 형식이 '{2}'인 정적 메서드)를 찾을 수 없습니다. @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + 이 컬렉션의 요소 형식은 ref 구조체 또는 ref 구조체를 허용하는 형식 매개 변수일 수 없습니다. + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + C# {2}의 '{0}' 값 '{1}'이(가) 잘못되었습니다. 언어 버전 '{3}' 이상을 사용하세요. @@ -902,6 +922,11 @@ 식 트리에는 튜플 == 또는 != 연산자를 사용할 수 없습니다. + + An expression tree may not contain a union conversion. + 식 트리에는 공용 구조체 변환을 사용할 수 없습니다. + + An expression tree may not contain a with-expression. 식 트리에는 with 식이 포함될 수 없습니다. @@ -1237,6 +1262,16 @@ 인라인 배열 요소 필드는 필수, 읽기 전용, 휘발성 또는 고정 크기 버퍼로 선언할 수 없습니다. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + 단일 매개 변수를 사용하여 명시적으로 선언된 공용 생성자는 'union' 선언에서 허용되지 않습니다. + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + 인스턴스 필드, 자동 속성 또는 필드와 유사한 이벤트는 'union' 선언에서 허용되지 않습니다. + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter '{0}': 명명되지 않은 수신기 매개 변수를 사용하여 확장 블록에 인스턴스 멤버를 선언할 수 없습니다. @@ -1467,6 +1502,16 @@ '{0}'에 '{1}'에 대한 정의가 없고, '{1}' 형식의 첫 번째 인수를 받는 액세스 가능한 확장 메서드 '{0}'을(를) 찾을 수 없습니다(대신 'await foreach'를 사용하여 비동기 컬렉션을 반복하려고 한 건가요?). 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + 암시적 참조 또는 boxing 변환을 통해 '{0}' 형식을 'object'로 변환할 수 없습니다. + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute는 이 기호에 적용할 수 없습니다. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. 대상 런타임이 확장 레이아웃 형식을 지원하지 않습니다. @@ -1857,11 +1902,6 @@ 'warnings', 'annotations' 또는 지시문의 끝이 필요합니다. - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - C# {2}의 '{0}' 값 '{1}'이(가) 잘못되었습니다. 언어 버전 '{3}' 이상을 사용하세요. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. 언어 버전 '{0}' 이상이 사용되는 경우가 아니면 nullable 형식 매개 변수가 값 형식 또는 nullable이 아닌 참조 형식으로 인식되어야 합니다. 언어 버전을 변경하거나 'class', 'struct' 또는 형식 제약 조건을 추가해 보세요. @@ -2677,6 +2717,21 @@ 예기치 않은 매개 변수 목록입니다. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + 'union' 선언에서 선언된 생성자에는 합성된 생성자 또는 명시적으로 선언된 생성자를 호출하는 'this' 이니셜라이저가 있어야 합니다. + + + + A union declaration must specify at least one case type. + 공용 구조체 선언은 하나 이상의 케이스 형식을 지정해야 합니다. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + 이 패턴에서는 '{0}' 형식의 식을 처리할 수 없습니다. 이 위치에서 추가 오류를 참조하세요. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. '{0}'에는 'UnmanagedCallersOnly' 특성을 지정할 수 없으며 이 항목은 직접 호출할 수 없습니다. 이 메서드에 대한 함수 포인터를 가져오세요. @@ -2687,11 +2742,36 @@ '{0}'에는 'UnmanagedCallersOnly' 특성이 지정되어 있으며 이 항목은 대리자 형식으로 변환할 수 없습니다. 이 메서드에 대한 함수 포인터를 가져오세요. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - '{0}'은(는) 인식할 수 없는 RefSafetyRulesAttribute 버전이 있는 모듈에 정의되어 있으며 '11'이 필요합니다. + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + '{0}'은(는) 인식할 수 없는 {1} 버전의 모듈에 정의되어 있으며, '{2}' 버전을 기대합니다. + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + '{2}'의 형식 매개 변수 '{1}'의 'new()' 제약 조건을 충족하려면 'RequiresUnsafe' 또는 'extern'으로 표시된 생성자 '{0}'에 안전하지 않은 컨텍스트가 필요합니다. + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + '{0}'은(는) 'RequiresUnsafe' 또는 'extern'으로 표시되어 있어 안전하지 않은 컨텍스트에서 사용해야 합니다. + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + '{0}'은(는) 시그니처에 포인터가 있으므로 안전하지 않은 컨텍스트에서 사용해야 합니다. + + This operation may only be used in an unsafe context + 이 작업은 안전하지 않은 컨텍스트에서만 사용할 수 있습니다. + + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + SkipLocalsInit 내에서 이니셜라이저 없는 stackalloc 식은 안전하지 않은 컨텍스트에서만 사용할 수 있습니다. + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. 구현된 멤버 '{0}'에 이 특성이 없으므로 UnscopedRefAttribute를 인터페이스 구현에 적용할 수 없습니다. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + 컬렉션 식 인수 @@ -3077,6 +3157,16 @@ nameof 연산자의 바인딩되지 않은 제네릭 형식 + + unions + 공용 구조체 + + + + updated memory safety rules + 업데이트된 메모리 안전 규칙 + + unsigned right shift 부호 없는 오른쪽 시프트 @@ -3862,6 +3952,16 @@ 구조체 멤버는 참조로 'this' 또는 다른 인스턴스 멤버를 반환합니다. + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute는 업데이트된 메모리 안전 규칙에서만 유효합니다. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute는 업데이트된 메모리 안전 규칙에서만 유효합니다. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. 매개 변수 '{0}'이(가) null이 아니기 때문에 반환 값은 null이 아니어야 합니다. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4211,7 +4311,7 @@ 디버깅 유형 지정('full'은 기본값, 'portable'은 플랫폼 간 형식, 'embedded'는 대상 .dll 또는 .exe에 포함된 - 플랫폼 간간 형식입니다) + 플랫폼 간 형식입니다) -optimize[+|-] 최적화 활성화(약식: -o) -deterministic 결정적 어셈블리 생성 (모듈 버전 GUID 및 타임스탬프 포함) @@ -4227,7 +4327,7 @@ -warn:<n> 경고 수준 설정(0 이상)(약식: -w) -nowarn:<warn list> 특정 경고 메시지 비활성화 (모든 Null 허용 여부에 대해 "nullable" 사용) --ruleset:<file> 특정 진단을 사용하지 않도록 설정하는 규칙 집합 파일을 +-ruleset:<file> 특정 진단을 비활성화하는 규칙 집합 파일을 지정합니다. -errorlog:<file>[,version=<sarif_version>] 모든 컴파일러 및 분석기의 진단을 로그할 파일을 @@ -4245,7 +4345,7 @@ : -d) -langversion:? 언어 버전에 허용되는 값 표시 -langversion:<string> 다음과 같은 언어 버전 지정 - 'latest'(부 버전을 포함한 최신 버전), + `latest`(부 버전을 포함한 최신 버전), `default`(`latest`와 동일), `latestmajor`(부 버전을 제외한 최신 버전), `preview`(지원되지 않는 미리 보기의 기능을 포함한 최신 버전), @@ -4275,7 +4375,7 @@ -baseaddress:<address> 빌드할 라이브러리의 기준 주소 -checksumalgorithm:<alg> PBD에 저장된 소스 파일 체크섬을 계산하기 위한 알고리즘을 지정합니다. 지원되는 값은 - SHA1 또는 SHA256(기본값)입니다. + SHA-1, SHA256(기본값), SHA384 또는 SHA512입니다. -codepage:<n> 소스를 열 때 사용할 코드페이지 지정 파일 -utf8output UTF-8 인코딩으로 컴파일러 메시지 출력 @@ -4293,14 +4393,14 @@ -errorendlocation 각 오류의 끝 위치의 행 및 열 출력 -preferreduilang 기본 출력 언어 이름을 지정합니다. --nosdkpath 표준 라이브러리 어셈블리의 기본 SDK 경로 검색을 사용하지 않도록 설정합니다. +-nosdkpath 표준 라이브러리 어셈블리의 기본 SDK 경로 검색을 비활성화합니다. -sdkpath:<path> 표준 라이브러리 어셈블리를 검색하는 데 사용되는 경로입니다. -nostdlib[+|-] 표준 라이브러리(mscorlib.dll)를 참조하지 않음 -subsystemversion:<string> 이 어셈블리의 하위 시스템 버전 지정 -lib:<file list> 참조를 검색할 추가 디렉터리 지정 --errorreport:<string> 내부 컴파일러 오류 처리 방법을 지정합니다 - (prompt, send, queue 또는 none). 기본값은 +-errorreport:<string> 내부 컴파일러 오류 처리 방법을 지정합니다. + prompt, send, queue 또는 none. 기본값은 queue입니다. -appconfig:<file> 어셈블리 바인딩 설정이 포함된 애플리케이션 구성 파일 지정 @@ -5855,6 +5955,16 @@ 매개 변수를 읽을 수 없습니다. 이 매개 변수를 사용하여 해당 이름으로 속성을 초기화했는지 확인하세요. + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + '안전하지 않은' 한정자는 현재 메모리 안전 규칙에서 아무런 영향을 주지 않습니다. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + '안전하지 않은' 한정자는 현재 메모리 안전 규칙에서 아무런 영향을 주지 않습니다. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute는 C# 11 이상 또는 net7.0 이상을 대상으로 하는 경우에만 유효합니다. @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - '{0}'의 partial 선언은 모든 클래스, 모든 레코드 클래스, 모든 구조체, 모든 레코드 구조체 또는 모든 인터페이스여야 합니다. + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + '{0}'의 부분 선언은 모든 클래스, 모든 레코드 클래스, 모든 구조체, 모든 공용 구조체, 모든 레코드 구조체 또는 모든 인터페이스여야 합니다. diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf index 9c7302e975eb..b505aa31b9bf 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pl.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + Żadne przeciążenie metody „{0}” nie przyjmuje {1} argumentów elementu „with(...)” @@ -387,6 +387,21 @@ Atrybut AsyncMethodBuilder jest niedozwolony w metodach anonimowych bez jawnego zwracanego typu. + + Unsafe member '{0}' cannot implement safe member '{1}' + Niezabezpieczony element członkowski „{0}” nie może zaimplementować zabezpieczonego elementu członkowskiego „{1}” + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + Niezabezpieczony element członkowski „{0}” nie może niejawnie zaimplementować zabezpieczonego elementu członkowskiego „{1}” + + + + Unsafe member '{0}' cannot override safe member '{1}' + Niezabezpieczony element członkowski „{0}” nie może przesłonić zabezpieczonego elementu członkowskiego „{1}” + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. Nie można użyć atrybutu "OverloadResolutionPriorityAttribute" w tej składowej. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + Argumenty elementu „with(...)” nie mogą być dynamiczne 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + Element „with(...)” dla interfejsu tylko do odczytu musi być pusty, jeśli jest obecny 'with(...)' element must be the first element - 'with(...)' element must be the first element + Element „with(...)” musi być pierwszym elementem 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + Elementy „with(...)” nie są obsługiwane dla typu „{0}” @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - Nie można odnaleźć dostępnej metody „{0}” z oczekiwanym podpisem: metoda statyczna z pojedynczym parametrem typu „ReadOnlySpan<{1}>” i zwracanym typem „{2}”. + Nie można odnaleźć dostępnej metody „{0}” z oczekiwanym podpisem: metoda statyczna, której ostatni parametr jest typu „ReadOnlySpan<{1}>” i zwracanym typem „{2}”. @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + Typ elementu tej kolekcji nie może być strukturą ref ani parametrem typu dopuszczającym struktury ref + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + Nieprawidłowa wartość „{0}”: „{1}” dla języka C# {2}. Użyj wersji języka „{3}” lub nowszej. @@ -902,6 +922,11 @@ Drzewo wyrażenia nie może zawierać operatora == ani != krotki. + + An expression tree may not contain a union conversion. + Drzewo wyrażenia nie może zawierać konwersji unii. + + An expression tree may not contain a with-expression. Drzewo wyrażeń nie może zawierać wyrażenia with. @@ -1237,6 +1262,16 @@ Pole elementu tablicy wbudowanej nie może być zadeklarowane jako wymagane, tylko do odczytu, nietrwałe ani jako bufor o stałym rozmiarze. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + Jawnie zadeklarowane konstruktory publiczne z pojedynczym parametrem nie są dozwolone w deklaracji „union”. + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + Pola wystąpienia, właściwości automatyczne lub zdarzenia podobne do pól nie są dozwolone w deklaracji „union”. + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter „{0}”: nie można deklarować elementów członkowskich wystąpienia w bloku rozszerzenia z nienazwanym parametrem odbiornika @@ -1467,6 +1502,16 @@ „{0}” nie zawiera definicji dla „{1}” i nie znaleziono dostępnej metody rozszerzenia „{1}”, która przyjmowałaby pierwszy argument typu „{0}” (czy chodziło Ci o iterację kolekcji asynchronicznej za pomocą polecenia „await foreach”?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + Nie można przekonwertować typu „{0}” na „object” za pomocą niejawnego odwołania lub konwersji typu boxing + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + Nie można zastosować atrybutu RequiresUnsafeAttribute do tego symbolu. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. Docelowe środowisko uruchomieniowe nie obsługuje rozszerzonych typów układu. @@ -1857,11 +1902,6 @@ Oczekiwano opcji „warnings” lub „annotations” albo końca dyrektywy - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - Nieprawidłowa wartość „{0}”: „{1}” dla języka C# {2}. Użyj wersji języka „{3}” lub nowszej. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. Parametr typu przyjmującego wartość null musi być znany jako typ wartości lub typ odwołania niedopuszczający wartości null, chyba że zostanie użyta wersja języka „{0}” lub nowsza. Rozważ zmianę wersji języka lub dodanie ograniczenia „class”, „struct” lub „type”. @@ -2677,6 +2717,21 @@ Nieoczekiwana lista parametrów. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + Konstruktor zadeklarowany w deklaracji „union” musi mieć inicjator „this”, który wywołuje konstruktor syntetyzowany lub jawnie zadeklarowany konstruktor. + + + + A union declaration must specify at least one case type. + Deklaracja unii musi określać co najmniej jeden typ przypadku. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + Wyrażenie typu „{0}” nie może być obsługiwane przez ten wzorzec. Zobacz dodatkowe błędy w tej lokalizacji. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. Element „{0}” ma atrybut „UnmanagedCallersOnly” i nie można go wywołać bezpośrednio. Uzyskaj wskaźnik funkcji do tej metody. @@ -2687,11 +2742,36 @@ Element „{0}” ma atrybut „UnmanagedCallersOnly” i nie można go przekonwertować na typ delegowany. Uzyskaj wskaźnik funkcji do tej metody. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - Element '{0}' jest zdefiniowany w module z nierozpoznaną wersją RefSafetyRulesAttribute, oczekiwano „11”. + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + Atrybut „{0}” jest zdefiniowany w module z nierozpoznaną wersją {1}, ponieważ oczekiwano „{2}”. + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + Niebezpieczny kontekst jest wymagany dla konstruktora „{0}” oznaczonego jako „RequiresUnsafe” lub „extern”, aby spełnić ograniczenie „new()” parametru typu „{1}” w „{2}” + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + Atrybut „{0}” musi być używany w niezabezpieczonym kontekście, ponieważ jest oznaczony jako „RequiresUnsafe” lub „extern” + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + Atrybut „{0}” musi być używany w niezabezpieczonym kontekście, ponieważ ma wskaźniki w sygnaturze + + This operation may only be used in an unsafe context + Ta operacja może być używana tylko w niezabezpieczonym kontekście + + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + wyrażenie stackalloc bez inicjatora wewnątrz elementu SkipLocalsInit może być używane tylko w niezabezpieczonym kontekście + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. Nie można zastosować atrybutu UnscopedRefAttribute do implementacji interfejsu, ponieważ zaimplementowana składowa „{0}” nie ma tego atrybutu. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + argumenty wyrażenia kolekcji @@ -3077,6 +3157,16 @@ niepowiązane typy ogólne w operatorze nameof + + unions + unie + + + + updated memory safety rules + zaktualizowano reguły zabezpieczeń pamięci + + unsigned right shift niepodpisane przesunięcie w prawo @@ -3862,6 +3952,16 @@ Składowa struktury zwraca element „this” lub inne składowe wystąpienia według odwołania + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + Atrybut RequiresUnsafeAttribute jest prawidłowy tylko w ramach zaktualizowanych reguł zabezpieczeń pamięci. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + Atrybut RequiresUnsafeAttribute jest prawidłowy tylko w ramach zaktualizowanych reguł zabezpieczeń pamięci. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. Wartość zwracana musi być inna niż null, ponieważ parametr „{0}” ma wartość inną niż null. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4203,7 +4303,7 @@ -resource:<resinfo> Osadzanie określonego zasobu (skrócona postać: -res) -linkresource:<resinfo> Łączenie określonego zasobu z tym zestawem (skrócona postać: -linkres) Gdzie format resinfo - jest <file>[,<name>[,public|private]] + to <file>[,<name>[,public|private]] - GENEROWANIE KODU - -debug[+|-] Emitowanie informacji o debugowaniu @@ -4245,11 +4345,11 @@ postać: -d) -langversion:? Wyświetlanie dozwolone wartości dla wersji językowej -langversion:<string> Określanie wersji języka, na przykład - „latest” (najnowsza wersja, w tym wersje pomocnicze), - „default” (tak samo jak „latest”), - „latestmajor” (najnowsza wersja, z wyjątkiem wersji pomocniczych), - „preview” (najnowsza wersja, w tym funkcje w nieobsługiwanym wersji zapoznawczej), - lub dokładnych wersji, takich jak „6” lub „7.1” + `latest` (najnowsza wersja, w tym wersje pomocnicze), + `default` (tak samo jak `latest`), + `latestmajor` (najnowsza wersja, z wyjątkiem wersji pomocniczych), + `preview` (najnowsza wersja, w tym funkcje w nieobsługiwanym wersji zapoznawczej), + lub dokładnych wersji, takich jak `6` lub `7.1` -nullable[+|-] Określanie włączenia|wyłączenia opcji kontekstu dopuszczania wartości null. -nullable:{enable|disable|warnings|annotations} Określanie opcji kontekstu dopuszczania wartości null enable|disable|warnings|annotations. @@ -4275,7 +4375,7 @@ -baseaddress:<address> Adres podstawowy dla bibiloteki, która ma być utworzona -checksumalgorithm:<alg> Określanie algorytmu obliczania pliku źródłowego pliku źródłowego przechowywanego w pliku PDB. Obsługiwane wartości to: - SHA1 lub SHA256 (wartość domyślna). + SHA1, SHA256 (domyślne), SHA384 lub SHA512. -codepage:<n> Określanie strony kodowej do używania podczas otwierania plików źródłowych -utf8output Komunikaty kompilatora danych wyjściowych w kodowaniu UTF-8 @@ -4300,7 +4400,7 @@ -lib:<file list> Określanie dodatkowych katalogów do wyszukania odwołań -errorreport:<string> Określanie sposobu obsługi wewnętrznych błędów kompilatora: - monit, wysyłanie, kolejka lub brak. Wartością domyślną jest + monit, wysyłanie, kolejka lub brak. Wartością domyślną to kolejka. -appconfig:<file> Określanie pliku konfiguracji aplikacji zawierającego ustawienia powiązania zestawu @@ -5855,6 +5955,16 @@ Parametr nie został odczytany. Czy zapomniano użyć go do zainicjowania właściwości o tej nazwie? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Modyfikator „unsafe” nie ma tutaj żadnego wpływu w bieżących regułach zabezpieczeń pamięci. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Modyfikator „unsafe” nie ma tutaj żadnego wpływu w bieżących regułach zabezpieczeń pamięci. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. Atrybut UnscopedRefAttribute jest prawidłowy tylko w języku C# 11 lub nowszym albo w przypadku platformy docelowej net7.0 lub nowszej. @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - Wszystkie częściowe deklaracje elementu "{0}" muszą być klasami, klasami rekordów, strukturami rekordów lub interfejsami + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + Częściowe deklaracje elementu „{0}” muszą być wszystkimi klasami, wszystkimi klasami rekordów, wszystkimi strukturami, wszystkimi uniami, wszystkimi strukturami rekordów lub wszystkimi interfejsami diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf index 1c5764b68e08..1a13d86ef5a1 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.pt-BR.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + Nenhuma sobrecarga para o método "{0}" usa {1} argumentos de elemento "with(...)" @@ -387,6 +387,21 @@ O atributo AsyncMethodBuilder não é permitido em métodos anônimos sem um tipo de retorno explícito. + + Unsafe member '{0}' cannot implement safe member '{1}' + O membro não seguro "{0}" não pode implementar o membro seguro "{1}" + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + O membro não seguro "{0}" não pode implementar implicitamente o membro seguro "{1}" + + + + Unsafe member '{0}' cannot override safe member '{1}' + O membro não seguro "{0}" não pode substituir o membro seguro "{1}" + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. Não é possível usar 'OverloadResolutionPriorityAttribute' neste membro. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + Argumentos de elemento "with(...)" não podem ser dinâmicos 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + O elemento "with(...)" para uma interface somente leitura deve estar vazio, se presente 'with(...)' element must be the first element - 'with(...)' element must be the first element + O elemento "with(...)" deve ser o primeiro elemento 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + Não há suporte para elementos "with(...)" no tipo "{0}" @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - Não foi possível encontrar um método '{0}' acessível com a assinatura esperada: um método estático com um único parâmetro do tipo 'ReadOnlySpan<{1}>' e tipo de retorno '{2}'. + Não foi possível localizar um método "{0}" acessível com a assinatura esperada: um método estático cujo último parâmetro é do tipo "ReadOnlySpan<{1}>" e do tipo de retorno "{2}". @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + O tipo de elemento dessa coleção não pode ser um ref struct nem um parâmetro de tipo que permita ref structs + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + Valor de '{0}' inválido: '{1}' para C# {2}. Use a versão da linguagem '{3}' ou superior. @@ -902,6 +922,11 @@ Uma árvore de expressão não pode conter um operador == ou != de tupla + + An expression tree may not contain a union conversion. + Uma árvore de expressão não pode conter uma conversão de união. + + An expression tree may not contain a with-expression. Uma árvore de expressão não pode conter uma expressão with. @@ -1237,6 +1262,16 @@ O campo de elemento da matriz em linha não pode ser declarado como obrigatório, somente leitura, volátil ou como um buffer de tamanho fixo. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + Construtores públicos declarados explicitamente com um único parâmetro não são permitidos em uma declaração de 'união'. + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + Campos de instância, propriedades automáticas ou eventos semelhantes a campos não são permitidos em uma declaração de 'união'. + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter "{0}": não é possível declarar membros de instância em um bloco de extensão com um parâmetro de receptor sem nome @@ -1467,6 +1502,16 @@ '{0}' não contém uma definição para '{1}' e nenhum método de extensão acessível '{1}' que aceite um primeiro argumento do tipo '{0}' pôde ser encontrado (você quis iterar sobre a coleção assíncrona usando 'await foreach'?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + Não é possível converter o tipo '{0}' em 'object' por meio de uma conversão implícita de referência ou boxing + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute não pode ser aplicado a este símbolo. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. O runtime de destino não dá suporte a tipos de layout estendidos. @@ -1857,11 +1902,6 @@ 'Warnings', 'annotations' ou fim de diretiva esperado - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - Valor de '{0}' inválido: '{1}' para C# {2}. Use a versão da linguagem '{3}' ou superior. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. Um parâmetro de tipo que permite valor nulo precisa ser conhecido como um tipo de valor ou um tipo de referência não anulável, a menos que seja usada a versão da linguagem '{0}' ou superior. Considere alterar a versão da linguagem ou adicionar uma restrição 'class', 'struct' ou de tipo. @@ -2677,6 +2717,21 @@ Lista de parâmetros inesperada. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + Um construtor declarado em uma declaração de 'união' deve ter um inicializador 'this' que chame um construtor sintetizado ou um construtor declarado explicitamente. + + + + A union declaration must specify at least one case type. + Uma declaração de união deve especificar pelo menos um tipo de caso. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + Uma expressão do tipo '{0}' não pode ser manipulada por este padrão, confira erros adicionais nesta localização. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. '{0}' foi atribuído com 'UnmanagedCallersOnly' e não pode ser chamado diretamente. Obtenha um ponteiro de função para esse método. @@ -2687,11 +2742,36 @@ '{0}' foi atribuído com 'UnmanagedCallersOnly' e não pode ser convertido em um tipo delegado. Obtenha um ponteiro de função para esse método. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - "{0}" é definido em um módulo com uma versão RefSafetyRulesAttribute não reconhecida, esperando "11". + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + "{0}" está definido em um módulo com uma versão não {1} reconhecida, esperando "{2}". + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + Um contexto não seguro é necessário para o construtor ''{0}'' marcado como ''RequiresUnsafe'' ou ''extern'' para satisfazer a restrição ''new()'' do parâmetro de tipo ''{1}'' em ''{2}'' + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + "{0}" deve ser usado em um contexto não seguro porque está marcado como "RequiresUnsafe" ou "extern" + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + "{0}" deve ser usado em um contexto não seguro porque tem ponteiros em sua assinatura + + + + This operation may only be used in an unsafe context + Esta operação só pode ser usada em um contexto não seguro + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + A expressão stackalloc sem um inicializador dentro de SkipLocalsInit só pode ser usada em um contexto não seguro + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. UnscopedRefAttribute não pode ser aplicado a uma implementação de interface porque o membro implementado "{0}" não tem esse atributo. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + argumentos de expressão de coleção @@ -3077,6 +3157,16 @@ tipos genéricos não associados no operador nameof + + unions + uniões + + + + updated memory safety rules + regras de segurança de memória atualizadas + + unsigned right shift deslocamento direito não atribuído @@ -3862,6 +3952,16 @@ O membro do struct retorna 'this' ou outros membros da instância por referência + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute só é válido de acordo com as regras de segurança de memória atualizadas. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute só é válido de acordo com as regras de segurança de memória atualizadas. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. O valor retornado precisa ser não nulo porque o parâmetro '{0}' não é nulo. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4207,7 +4307,7 @@ - GERAÇÃO DE CÓDIGO - -debug[+|-] Emitir informações de depuração --debug:{full|pdbonly|portable|embedded} +-debug:{full|pdscriptly|portable|embedded} Especificar o tipo de depuração ("full" é padrão, "portable" é um formato de multiplataforma, "embedded" é um formato de multiplataforma inserido @@ -4275,7 +4375,7 @@ -baseaddress:<address> Endereço base da biblioteca a ser criada -checksumalgorithm:<alg> Especificar o algoritmo para calcular a soma de verificação do arquivo de origem soma de verificação armazenada no PDB. Os valores suportados são: - SHA1 ou SHA256 (padrão). + SHA1, SHA256 (padrão), SHA384 ou SHA512. -codepage:<n> Especificar a página de código a ser usada ao abrir os arquivos do código-fonte -utf8output Mensagens do compilador de saída na codificação UTF-8 @@ -4293,7 +4393,7 @@ -errorendlocation Linha de saída e coluna do local final de cada erro -preferreduilang Especificar o nome do idioma de saída preferido. --nosdkpath Desabilitar a pesquisa do caminho padrão do SDK nos conjuntos de bibliotecas padrão. +-nosdkpath Desabilite a pesquisa do caminho padrão do SDK nos conjuntos de bibliotecas padrão. -sdkpath:<path> Caminho usado para pesquisar assemblies de biblioteca padrão. -nostdlib[+|-] Não referenciar à biblioteca padrão (mscorlib.dll) -subsystemversion:<string> Especificar a versão do subsistema deste assembly @@ -5855,6 +5955,16 @@ O parâmetro não foi lido. Você esqueceu de usá-lo para inicializar a propriedade com esse nome? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + O modificador 'unsafe' não tem efeito aqui, segundo as regras de segurança de memória atuais. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + O modificador 'unsafe' não tem efeito aqui, segundo as regras de segurança de memória atuais. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute só é válido em C# 11 ou posterior ou ao direcionar para net7.0 ou posterior. @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - As declarações parciais de '{0}' precisam ser todas classes, todas classes de registros, structs, todos registros de structs ou todas as interfaces + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + Declarações parciais de '{0}' devem ser todas classes, todas classes de registro, todas structs, todas uniões, todas structs de registro ou todas interfaces diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf index 68117e107aba..e4f39f0996f7 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.ru.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + Ни одна из перегрузок метода "{0}" не принимает аргументы элемента "with(...)" ({1}) @@ -387,6 +387,21 @@ Атрибут AsyncMethodBuilder запрещен для анонимных методов без явного типа возвращаемого значения. + + Unsafe member '{0}' cannot implement safe member '{1}' + Небезопасный элемент "{0}" не может реализовывать безопасный элемент "{1}" + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + Небезопасный элемент "{0}" не может неявно реализовывать безопасный элемент "{1}" + + + + Unsafe member '{0}' cannot override safe member '{1}' + Небезопасный элемент "{0}" не может переопределять безопасный элемент "{1}" + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. Невозможно использовать "OverloadResolutionPriorityAttribute" с этим членом. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + Аргументы элемента "with(...)" не могут быть динамическими 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + Элемент "with(...)" для интерфейса только для чтения должен быть пустым, если он присутствует 'with(...)' element must be the first element - 'with(...)' element must be the first element + Элемент "with(...)" должен быть первым элементом 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + Элементы "with(...)" не поддерживаются для типа "{0}" @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - Не удалось найти доступный метод "{0}" с ожидаемой подписью: статический метод с одним параметром типа "ReadOnlySpan<{1}>" и типом возвращаемого значения "{2}". + Не удалось найти доступный метод "{0}" с ожидаемой подписью: статический метод с последним параметром типа "ReadOnlySpan<{1}>" и типом возвращаемого значения "{2}". @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + Тип элемента этой коллекции не может быть ссылочной структурой или параметром типа, допускающим ссылочные структуры + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + Недопустимое значение "{0}": "{1}" для C# {2}. Используйте версию языка "{3}" или более позднюю. @@ -902,6 +922,11 @@ Дерево выражений не может содержать оператор == или != кортежа. + + An expression tree may not contain a union conversion. + Дерево выражений не может содержать преобразование объединения. + + An expression tree may not contain a with-expression. Дерево выражения не может содержать выражение with. @@ -1237,6 +1262,16 @@ Поле элемента встроенного массива нельзя объявить как обязательное, доступное только для чтения, переменное или как буфер фиксированного размера. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + Явно объявленные общедоступные конструкторы с одним параметром не допускаются в объявлении "union". + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + Поля экземпляра, автоматические свойства или события, похожие на поля, не допускаются в объявлении "union". + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter "{0}": невозможно объявить члены экземпляра в блоке расширения с параметром получателя без имени @@ -1467,6 +1502,16 @@ "{0}" не содержит определение для "{1}", не найден доступный метод расширения "{1}", принимающий первый аргумент типа "{0}" (возможно, нужно было выполнить итерацию для синхронной коллекции с помощью "await foreach"?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + Невозможно преобразовать тип "{0}" в "object" с помощью преобразования неявной ссылки или преобразования-упаковки + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute невозможно применить к этому символу. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. Целевая среда выполнения не поддерживает расширенные типы структуры. @@ -1857,11 +1902,6 @@ Ожидаемые значения: "warnings", "annotations" или конец директивы - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - Недопустимое значение "{0}": "{1}" для C# {2}. Используйте версию языка "{3}" или более позднюю. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. Параметр типа, допускающий значение null, должен представлять собой тип значения или ссылку на тип, не допускающую значение null, если не используется версия языка "{0}" или выше. Попробуйте изменить версию языка или добавить ограничение "class", "struct" или ограничение типа. @@ -2677,6 +2717,21 @@ Неожиданный список параметров. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + Конструктор, объявленный в объявлении "union", должен содержать инициализатор "this", который вызывает синтезированный или явно объявленный конструктор. + + + + A union declaration must specify at least one case type. + В объявлении объединения должен быть указан как минимум один тип обращения. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + Выражение типа "{0}" невозможно обработать этим шаблоном. См. дополнительные ошибки в этом расположении. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. "{0}" имеет атрибут "UnmanagedCallersOnly" и не может вызываться напрямую. Получите указатель на функцию для этого метода. @@ -2687,11 +2742,36 @@ "{0}" имеет атрибут "UnmanagedCallersOnly" и не может быть преобразован в тип делегата. Получите указатель на функцию для этого метода. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - "{0}" определен в модуле с нераспознанной версией RefSafetyRulesAttribute. Ожидается "11". + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + "{0}" определен в модуле с нераспознанной версией {1}, ожидается "{2}". + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + Для конструктора "{0}", помеченного как "RequiresUnsafe" или "extern", требуется небезопасный контекст, чтобы удовлетворить ограничение "new()" параметра типа "{1}" в "{2}" + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + "{0}" должен использоваться в небезопасном контексте, так как он помечен как RequiresUnsafe или extern + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + "{0}" должен использоваться в небезопасном контексте, так как в его сигнатуре есть указатели + + This operation may only be used in an unsafe context + Эту операцию можно использовать только в небезопасном контексте + + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + Выражение stackalloc без инициализатора внутри SkipLocalsInit можно использовать только в небезопасном контексте + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. UnscopedRefAttribute нельзя применить к реализации интерфейса, поскольку реализованный элемент {0} не содержит этого атрибута. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + аргументы выражения коллекции @@ -3077,6 +3157,16 @@ не привязанные универсальные типы в операторе nameof + + unions + объединения + + + + updated memory safety rules + обновленные правила обеспечения безопасности памяти + + unsigned right shift сдвиг вправо без знака @@ -3862,6 +3952,16 @@ Элемент структуры возвращает "этот" или другие элементы экземпляра по ссылке + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute допустим только в рамках обновленных правил обеспечения безопасности памяти. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute допустим только в рамках обновленных правил обеспечения безопасности памяти. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. Возвращаемое значение должно быть отлично от NULL, так как параметр "{0}" имеет значение, отличное от NULL. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4176,7 +4276,7 @@ Itanium, x64, arm, arm64, anycpu32bitpreferred или anycpu. По умолчанию используется anycpu. - - ВХОДНЫЕ ФАЙЛЫ - + - ВХОДНЫЕ ФАЙЛЫ - --recurse:<подстановочный знак> Включить все файлы в текущем каталоге и вложенных каталогах в соответствии с подстановочными знаками @@ -4274,8 +4374,8 @@ - РАСШИРЕННЫЕ ПАРАМЕТРЫ - -baseaddress:<адрес> Базовый адрес для создаваемой библиотеки -checksumalgorithm:<алгоритм> Указать алгоритм вычисления контрольной суммы исходного файла, - хранящегося в PDB. Поддерживаемые значения: - SHA1 или SHA256 (по умолчанию). + исходного файла, хранящегося в PDB. Поддерживаемые значения: + SHA1, SHA256 (по умолчанию), SHA384 или SHA512. -codepage:<n> Указать кодовую страницу для использования при открытии исходных файлов -utf8output Выводить сообщения компилятора в кодировке UTF-8 @@ -4292,7 +4392,7 @@ имя выходного файла с расширением .pdb) -errorendlocation Строка вывода и столбец конечного расположения каждой ошибки --preferreduilang Указать предпочитаемое имя языка вывода. +-preferreduilang Указать предпочитаемое имя языка вывода. -nosdkpath Отключить поиск стандартных библиотечных сборок по пути SDK по умолчанию. -sdkpath:<путь> Путь, используемый для поиска стандартных сборок библиотек. -nostdlib[+|-] Не ссылаться на стандартную библиотеку (mscorlib.dll) @@ -4300,7 +4400,7 @@ -lib:<список файлов> Указать дополнительные каталоги для поиска ссылок -errorreport:<строка> Указать, как обрабатывать внутренние ошибки компилятора: - prompt (выводить запрос), send (отправлять), queue (ставить в очередь) или none (не выполнять никаких действий). Значение по умолчанию: + prompt, send, queue или none. Значение по умолчанию: queue. -appconfig:<файл> Указать файл конфигурации приложения, содержащий параметры привязки сборки @@ -5855,6 +5955,16 @@ Параметр не читается. Возможно, вы забыли использовать его для инициализации свойства с таким же именем? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Модификатор "unsafe" в данном случае не оказывает влияния в соответствии с действующими правилами безопасности памяти. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + Модификатор "unsafe" в данном случае не оказывает влияния в соответствии с действующими правилами безопасности памяти. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute допустим только в C# 11 или более поздней версии или при нацелии на net7.0 или более позднюю версию. @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - Все разделяемые объявления "{0}" должны относиться к одному типу (классы, классы записей, записи, структуры, структуры записей или интерфейсы) + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + Все разделяемые объявления "{0}" должны относиться к одному типу (классы, классы записей, записи, структуры, объединения, структуры записей или интерфейсы) diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf index 48f80aa717af..e5a2b406cdae 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.tr.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + '{0}' yöntemi için {1} ‘with(...)’ öğe bağımsız değişkenleri için aşırı yükleme yok @@ -387,6 +387,21 @@ Açık dönüş türü olmadan, anonim yöntemlerde AsyncMethodBuilder özniteliğine izin verilmez. + + Unsafe member '{0}' cannot implement safe member '{1}' + Güvenli olmayan '{0}' üyesi güvenli '{1}' üyesini uygulayamaz + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + Güvenli olmayan '{0}' üyesi güvenli '{1}' üyesini örtük olarak uygulayamaz + + + + Unsafe member '{0}' cannot override safe member '{1}' + Güvenli olmayan '{0}' üyesi güvenli '{1}' üyesini geçersiz kılamaz + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. Bu üyede 'OverloadResolutionPriorityAttribute' kullanılamaz. @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + 'with(...)' öğe bağımsız değişkenleri dinamik olamaz 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + Salt okunur arabirim için 'with(...)' öğesi varsa boş olmalıdır 'with(...)' element must be the first element - 'with(...)' element must be the first element + 'with(...)' öğesi ilk öğe olmalıdır 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + 'with(...)' öğeleri '{0}' türü için desteklenmiyor @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - Beklenen imzaya sahip erişilebilir bir '{0}' yöntemi bulunamadı: 'ReadOnlySpan<{1}>' türünde tek bir parametreye ve '{2}' dönüş türüne sahip statik bir yöntem. + Beklenen imzaya sahip erişilebilir bir '{0}' yöntemi bulunamadı: 'ReadOnlySpan<{1}>' türünde tek bir parametreye ve '{2}' dönüş türüne sahip statik bir yöntem. @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + Bu koleksiyonun öğe türü başvuru yapısı veya başvuru yapısına izin veren tür parametresi olamaz + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + C# {2} için geçersiz '{0}' değeri: '{1}'. Lütfen '{3}' veya daha yüksek bir dil sürümü kullanın. @@ -902,6 +922,11 @@ İfade ağacı, demetin == veya != işlecini içeremez. + + An expression tree may not contain a union conversion. + İfade ağacı, birleşim dönüştürmesi içeremez. + + An expression tree may not contain a with-expression. İfade ağacı, with ifadesi içeremez. @@ -1237,6 +1262,16 @@ Satır içi dizi öğesi alanı gerekli, salt okunur, geçici veya sabit boyutlu arabellek olarak bildirilemez. + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + 'union' bildiriminde tek parametreli açıkça bildirilen genel oluşturuculara izin verilmez. + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + 'union' bildiriminde örnek alanlara, otomatik özelliklere veya alan benzeri olaylara izin verilmez. + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter '{0}': adsız alıcı parametresi olan bir genişletme bloğunda örnek üyeler bildirilemez @@ -1467,6 +1502,16 @@ ' {0}' '{1}' için bir tanım içermiyor ve '{0}' türünden ilk bağımsız değişkeni kabul eden erişilebilir '{1}' uzantı yöntemi bulunamadı (zaman uyumsuz koleksiyon üzerinde 'await foreach' ile yineleme yapmak mı istediniz?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + '{0}' türü, örtülü başvuru veya paketleme dönüştürmesiyle 'object' türüne dönüştürülemez + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute bu sembole uygulanamaz. + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. Hedef çalışma zamanı, genişletilmiş düzen türlerini desteklemiyor. @@ -1857,11 +1902,6 @@ 'Uyarılar', 'ek açıklama' veya yönergenin sonu bekleniyordu - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - C# {2} için geçersiz '{0}' değeri: '{1}'. Lütfen '{3}' veya daha yüksek bir dil sürümü kullanın. - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. '{0}' dil sürümü veya daha yenisi kullanılmadığı sürece, null atanabilir tür parametresi bir değer türü veya null atanamaz başvuru türü olarak bilinmelidir. Dil sürümünü değiştirmeyi veya bir 'class', 'struct' ya da tür kısıtlaması eklemeyi düşünün. @@ -2677,6 +2717,21 @@ Beklenmeyen parametre listesi. + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + 'union' bildirimi içinde bildirilen bir oluşturucunun, sentezlenmiş oluşturucuyu veya açıkça bildirilmiş bir oluşturucuyu çağıran bir 'this' başlatıcısı olmalıdır. + + + + A union declaration must specify at least one case type. + 'union' bildirimi en az bir durum türü belirtmelidir. + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + '{0}' türündeki bir ifade bu desen tarafından işlenemez; bu konumdaki ek hatalara bakın. + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. '{0}', 'UnmanagedCallersOnly' ile ilişkilendirilmiş ve doğrudan çağrılamaz. Bu yöntem için bir işlev işaretçisi edinin. @@ -2687,11 +2742,36 @@ '{0}', 'UnmanagedCallersOnly' özniteliğine sahip ve temsilci türüne dönüştürülemez. Bu yöntem için bir işlev işaretçisi edinin. UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - '{0}' tanınmayan bir RefSafetyRulesAttribute sürümüne sahip bir modülde tanımlandı ve '11' bekleniyor. + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + '{0}' tanınmayan bir {1} sürümünde tanımlandı, '{2}' bekleniyor. + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + 'RequiresUnsafe' veya 'extern' olarak işaretlenen '{0}' oluşturucusu için, '{1}' içindeki '{2}' tür parametresinin 'new()' kısıtlamasını karşılamak üzere güvenli olmayan bir bağlam gereklidir + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + '{0}' 'RequiresUnsafe' veya 'extern' olarak işaretlendiği için güvenli olmayan bir bağlamda kullanılmalıdır + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + '{0}' işaretçileri olduğu için güvenli olmayan bir bağlamda kullanılmalıdır + + This operation may only be used in an unsafe context + Bu işlem yalnızca güvenli olmayan bir bağlamda kullanılabilir + + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + Başlatıcısı olmayan stackalloc ifadesi SkipLocalsInit içinde yalnızca güvenli olmayan bir bağlamda kullanılabilir + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. Uygulanan '{0}' üyesi bu özniteliğe sahip olmadığından UnscopedRefAttribute bir arabirim uygulamasına uygulanamıyor. @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + koleksiyon ifadesi bağımsız değişkenleri @@ -3077,6 +3157,16 @@ nameof işlecindeki bağlı olmayan genel türler + + unions + birleşimler + + + + updated memory safety rules + bellek güvenliği kuralları güncelleştirildi + + unsigned right shift işaretsiz sağ kaydırma @@ -3862,6 +3952,16 @@ Struct üyesi, 'tis' veya diğer örnek üyelerini referans olarak döndürür + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute yalnızca güncelleştirilmiş bellek güvenliği kuralları altında geçerlidir. + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute yalnızca güncelleştirilmiş bellek güvenliği kuralları altında geçerlidir. + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. '{0}' parametresi null olmadığından dönüş değeri de null olmamalıdır. @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4154,164 +4254,164 @@ accessibility errors with what assembly they came from. - Visual C# Compiler Options + Visual C# Derleyicisi Seçenekleri - - OUTPUT FILES - --out:<file> Specify output file name (default: base name of - file with main class or first file) --target:exe Build a console executable (default) (Short + - ÇIKIŞ DOSYALARI - +-out:<file> Çıkış dosyası adını belirtir (varsayılan: ana sınıf veya + ilk dosyayla birlikte dosyanın temel adı) +-target:exe Konsol yürütülebilir dosyası oluşturur (varsayılan) (Kısa form: -t:exe) --target:winexe Build a Windows executable (Short form: +-target:winexe Windows yürütülebilir dosyası oluşturur (Kısa biçimi: -t:winexe) --target:library Build a library (Short form: -t:library) --target:module Build a module that can be added to another - assembly (Short form: -t:module) --target:appcontainerexe Build an Appcontainer executable (Short form: +-target:library Kitaplık oluşturur (Kısa biçimi: -t:library) +-target:module Başka bir derlemeye eklenebilir + bir modül oluşturur (Kısa biçimi: -t:module) +-target:appcontainerexe Appcontainer yürütülebilir dosyası oluşturur (Kısa biçimi: -t:appcontainerexe) --target:winmdobj Build a Windows Runtime intermediate file that - is consumed by WinMDExp (Short form: -t:winmdobj) --doc:<file> XML Documentation file to generate --refout:<file> Reference assembly output to generate --platform:<string> Limit which platforms this code can run on: x86, - Itanium, x64, arm, arm64, anycpu32bitpreferred, or - anycpu. The default is anycpu. +-target:winmdobj WinMDExp kullanan Windows Çalışma Zamanı + ara dosyası oluşturur (Kısa biçimi: -t:winmdobj) tarafından +-doc:<file> oluşturulacak XML belgeleri dosyası +-refout:<file> Oluşturulacak başvuru derlemesi çıkışı +-platform:<string> Bu kodun çalışabileceği platformları sınırlar: x86, + Itanium, x64, arm, arm64, anycpu32bitpreferred veya + anycpu. Varsayılan: anycpu. - - INPUT FILES - --recurse:<wildcard> Include all files in the current directory and - subdirectories according to the wildcard - specifications --reference:<alias>=<file> Reference metadata from the specified assembly - file using the given alias (Short form: -r) --reference:<file list> Reference metadata from the specified assembly - files (Short form: -r) --addmodule:<file list> Link the specified modules into this assembly --link:<file list> Embed metadata from the specified interop - assembly files (Short form: -l) --analyzer:<file list> Run the analyzers from this assembly - (Short form: -a) --additionalfile:<file list> Additional files that don't directly affect code - generation but may be used by analyzers for producing - errors or warnings. --embed Embed all source files in the PDB. --embed:<file list> Embed specific files in the PDB. + - GİRİŞ DOSYALARI - +-recurse:<wildcard> Joker karakter belirtimlerine göre + geçerli dizindeki ve alt dizinlerdeki tüm + dosyaları ekler +-reference:<alias>=<file> Verilen diğer adı kullanan belirtilen derleme dosyasından + meta verilerine başvurur (Kısa biçimi: -r) +-reference:<file list> Belirtilen derleme dosyasından + başvuru meta verileri (Kısa biçimi: -r) +-addmodule:<file list> Belirtilen modülleri bu derlemeye bağlar +-link:<file_list> Belirtilen birlikte çalışma derleme dosyalarından + meta verileri ekler (Kısa biçimi: -l) +-analyzer:<file list> Bu derlemedeki çözümleyicileri çalıştırır + (Kısa biçimi: -a) +-additionalfile:<file list> Kod oluşturmayı doğrudan etkilemeyen ancak + hata veya uyarı üretimi için çözümleyiciler tarafından kullanılabilen + ek dosyalar. +-embed Tüm kaynak dosyaları PDB’ye ekler. +-embed:<file list> Belirli dosyaları PDB'ye ekler. - - RESOURCES - --win32res:<file> Specify a Win32 resource file (.res) --win32icon:<file> Use this icon for the output --win32manifest:<file> Specify a Win32 manifest file (.xml) --nowin32manifest Do not include the default Win32 manifest --resource:<resinfo> Embed the specified resource (Short form: -res) --linkresource:<resinfo> Link the specified resource to this assembly - (Short form: -linkres) Where the resinfo format - is <file>[,<string name>[,public|private]] + - KAYNAKLAR - +-win32res:<file> Win32 kaynak dosyasını (.res) belirtir +-win32icon:<file> Çıkış dosyası için bu simgeyi kullanır +-win32manifest:<file> Win32 bildirim dosyasını (.xml) belirtir +-nowin32manifest Varsayılan Win32 bildirimini ekler +-resource:<resinfo> Belirtilen kaynağı ekler (Kısa biçimi: -res) +-linkresource:<resinfo> Belirtilen kaynağı bu derlemeye bağlar + (Kısa biçimi: -linkres) Resinfo biçimi + <file>[,<string name>[,public|private]] - - CODE GENERATION - --debug[+|-] Emit debugging information + - KOD OLUŞTURMA - +-debug[+|-] Hata ayıklama bilgilerini gösterir -debug:{full|pdbonly|portable|embedded} - Specify debugging type ('full' is default, - 'portable' is a cross-platform format, - 'embedded' is a cross-platform format embedded into - the target .dll or .exe) --optimize[+|-] Enable optimizations (Short form: -o) --deterministic Produce a deterministic assembly - (including module version GUID and timestamp) --refonly Produce a reference assembly in place of the main output --instrument:TestCoverage Produce an assembly instrumented to collect - coverage information --sourcelink:<file> Source link info to embed into PDB. + Hata ayıklama türünü belirtir ('full' varsayılandır, + 'portable' platformlar arası biçimdir, + 'embedded', platformlar arası biçimdir ve hedef + .dll veya .exe dosyasına eklenir) +-optimize[+|-] İyileştirmeleri etkinleştirir (Kısa biçimi: -O) +-deterministic Belirlenimci bir derleme üretir + (modül sürümü GUID'si ve zaman damgası dahil) +-refonly Ana çıkış yerine bir başvuru derlemesi üretir +-instrument:TestCoverage Kapsam bilgilerini toplamak için işaretlenmiş + bir derleme üretir +-sourcelink:<file> PDB dosyasına eklenecek kaynak bağlantısı bilgileri. - - ERRORS AND WARNINGS - --warnaserror[+|-] Report all warnings as errors --warnaserror[+|-]:<warn list> Report specific warnings as errors - (use "nullable" for all nullability warnings) --warn:<n> Set warning level (0 or higher) (Short form: -w) --nowarn:<warn list> Disable specific warning messages - (use "nullable" for all nullability warnings) --ruleset:<file> Specify a ruleset file that disables specific - diagnostics. + - HATALAR VE UYARILAR - +-warnaserror[+|-] Tüm uyarıları hata olarak bildirir +-warnaserror[+|-]:<warn list> Belirli uyarıları hata olarak bildirir + (tüm null atanabilirlik uyarıları için "nullable" kullanın) +-warn:<n> Uyarı düzeyini ayarlar (0 veya üzeri) (Kısa biçimi: -w) +-nowarn:<warn list> Belirli uyarı iletilerini devre dışı bırakır + (tüm null atanabilirlik uyarıları için "nullable" kullanın) +-ruleset:<file> Belirli tanılamaları devre dışı bırakan bir + kural kümesi dosyası belirtir. -errorlog:<file>[,version=<sarif_version>] - Specify a file to log all compiler and analyzer - diagnostics. - sarif_version:{1|2|2.1} Default is 1. 2 and 2.1 - both mean SARIF version 2.1.0. --reportanalyzer Report additional analyzer information, such as - execution time. --skipanalyzers[+|-] Skip execution of diagnostic analyzers. + Tüm derleyici ve çözümleyici tanılamalarının günlüğe + kaydedileceği dosyayı belirtir. + sarif_version:{1|2|2.1} Varsayılan olarak 1. 2 ve 2.1 + Her ikisi ise 2.1.0 SARIF sürümünü ifade eder. +-reportanalyzer Yürütme zamanı gibi ek çözümleyici bilgilerini + raporlar. +-skipanalyzers[+|-] Tanılama çözümleyicilerinin yürütülmesini atlar. - - LANGUAGE - --checked[+|-] Generate overflow checks --unsafe[+|-] Allow 'unsafe' code --define:<symbol list> Define conditional compilation symbol(s) (Short - form: -d) --langversion:? Display the allowed values for language version --langversion:<string> Specify language version such as - `latest` (latest version, including minor versions), - `default` (same as `latest`), - `latestmajor` (latest version, excluding minor versions), - `preview` (latest version, including features in unsupported preview), - or specific versions like `6` or `7.1` --nullable[+|-] Specify nullable context option enable|disable. + - DİL - +-checked[+|-] Taşma denetimleri oluştur +-unsafe[+|-] 'Güvenli olmayan' koda izin ver +-define:<symbol list> Koşullu derleme sembollerini tanımlar (Kısa + biçimi: -d) +-langversion:? Dil sürümü için izin verilen değerleri görüntüler +-langversion:<string> `latest` (ikincil sürümler dahil en son sürüm), + `default` (`latest` ile aynı), + `latestmajor` (ikincil sürümler hariç en son sürüm), + `preview` (desteklenmeyen önizlemedeki özellikler dahil en son sürüm) + veya `6` ya da `7.1` benzeri belirli sürümler + gibi dil sürümünü belirtir +-nullable[+|-] Null atanabilir bağlam seçeneğini (enable|disable) belirtir. -nullable:{enable|disable|warnings|annotations} - Specify nullable context option enable|disable|warnings|annotations. + Null atanabilir bağlam seçeneğini (enable|disable|warnings|annotations) belirtir. - - SECURITY - --delaysign[+|-] Delay-sign the assembly using only the public - portion of the strong name key --publicsign[+|-] Public-sign the assembly using only the public - portion of the strong name key --keyfile:<file> Specify a strong name key file --keycontainer:<string> Specify a strong name key container --highentropyva[+|-] Enable high-entropy ASLR + - GÜVENLİK - +-delaysign[+|-] Tanımlayıcı ad anahtarının ortak bölümünü kullanarak + derlemeyi gecikmeli imzalar +-publicsign[+|-] Tanımlayıcı ad anahtarının ortak bölümünü kullanarak + derlemeyi genel imzalar +-keyfile:<file> Tanımlayıcı ad anahtarı dosyasını belirtir +-keycontainer:<string> Tanımlayıcı ad anahtarı kapsayıcısını belirtir +-highentropyva[+|-] Yüksek entropili ASLR’yi etkinleştirir - - MISCELLANEOUS - -@<file> Read response file for more options --help Display this usage message (Short form: -?) --nologo Suppress compiler copyright message --noconfig Do not auto include CSC.RSP file --parallel[+|-] Concurrent build. --version Display the compiler version number and exit. + - DİĞER - +@<file> Daha fazla seçenek için yanıt dosyasını okur +-help Bu kullanım iletisini görüntüler (Kısa biçimi: -?) +-nologo Derleyici telif hakkı iletisini gizler +-noconfig VBC.RSP dosyasını otomatik olarak eklemez +-parallel[+|-] Eş zamanlı derleme. +-version Derleyici sürüm numarasını görüntüler ve çıkar. - - ADVANCED - --baseaddress:<address> Base address for the library to be built --checksumalgorithm:<alg> Specify algorithm for calculating source file - checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). --codepage:<n> Specify the codepage to use when opening source - files --utf8output Output compiler messages in UTF-8 encoding --main:<type> Specify the type that contains the entry point - (ignore all other possible entry points) (Short - form: -m) --fullpaths Compiler generates fully qualified paths --filealign:<n> Specify the alignment used for output file - sections + - GELİŞMİŞ - +-baseaddress:<address> Oluşturulacak kitaplığın temel adresi +-checksumalgorithm:<alg> PDB’de depolanan sağlama toplamı kaynak dosyasını hesaplamak için + kullanılan algoritmayı belirtir. Desteklenen değerler şunlardır: + SHA1, SHA256 (varsayılan), SHA384 veya SHA512. +-codepage:<n> Kaynak açılırken kullanılacak kod sayfasını belirtir + Dosyalar +-utf8output UTF-8 kodlama kümesinde -utf8output Çıkış derleyicisi +-main:<type> Giriş noktasını içeren türü belirtir + (diğer tüm olası giriş noktalarını yoksay) (Kısa + biçimi: -m) +-fullpaths Derleyici mutlak yollar oluşturur +-filealign:<n> Çıkış dosyası bölümleri için kullanılan hizalamayı + belirtir -pathmap:<K1>=<V1>,<K2>=<V2>,... - Specify a mapping for source path names output by - the compiler. --pdb:<file> Specify debug information file name (default: - output file name with .pdb extension) --errorendlocation Output line and column of the end location of - each error --preferreduilang Specify the preferred output language name. --nosdkpath Disable searching the default SDK path for standard library assemblies. --sdkpath:<path> Path used to search for standard library assemblies. --nostdlib[+|-] Do not reference standard library (mscorlib.dll) --subsystemversion:<string> Specify subsystem version of this assembly --lib:<file list> Specify additional directories to search in for - references --errorreport:<string> Specify how to handle internal compiler errors: - prompt, send, queue, or none. The default is - queue. --appconfig:<file> Specify an application configuration file - containing assembly binding settings --moduleassemblyname:<string> Name of the assembly which this module will be - a part of --modulename:<string> Specify the name of the source module --generatedfilesout:<dir> Place files generated during compilation in the - specified directory. --reportivts[+|-] Output information on all IVTs granted to this - assembly by all dependencies, and annotate foreign assembly - accessibility errors with what assembly they came from. + Derleyicinin oluşturduğu kaynak yol adları için + eşleştirme belirtir. +-pdb:<file> Hata ayıklama bilgileri dosya adını belirtir (varsayılan: + .pdb uzantılı çıkış dosyası adı) +-errorendlocation Her hatanın çıkış satırı ve + bitiş konumu sütunu +-preferreduilang Tercih edilen çıkış dili adını belirtir. +-nosdkpath Standart kitaplık derlemeleri için varsayılan SDK yolunu aramayı devre dışı bırakır. +-sdkpath:<path> Standart kitaplık derlemelerini aramak için kullanılan yol. +-nostdlib[+|-] Standart kitaplıklara başvurmaz (mscorlib.dll) +-subsystemversion:<string> Bu derlemenin alt sistem sürümünü belirtir +-lib:<file list> Başvurular için aranacak ek dizinleri + belirtir +-errorreport:<string> İç derleyici hatalarının nasıl işleneceğini belirtir: + istem, gönderme, kuyruğa alma veya hiçbiri. Varsayılan: + kuyruğa alma. +-appconfig:<file> Derleme bağlama ayarlarını içeren + uygulama yapılandırma dosyasını belirtir +-moduleassemblyname:<string> Bu modülün bir parçası olacağı derlemenin + adı +-modulename:<string> Kaynak modülün adını belirtir +-generatedfilesout:<dir> Derleme sırasında oluşturulan dosyaları + belirtilen dizine yerleştirir. +-reportivts[+|-] Bu derlemeye tüm bağımlılıklar tarafından verilen tüm IVT'ler hakkında çıkış bilgisi + ve hangi derlemeden geldikleri dahil olmak üzere + yabancı derleme erişilebilirlik hataları hakkında ek açıklamalar. Visual C# Compiler Options @@ -5855,6 +5955,16 @@ Parametre okunmadı. Bu ada sahip özelliği başlatmak için bu parametreyi kullanmayı unutmuş olabilirsiniz. + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 'unsafe' değiştiricisi, mevcut bellek güvenliği kuralları altında burada herhangi bir etkiye sahip değildir. + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 'unsafe' değiştiricisi, mevcut bellek güvenliği kuralları altında burada herhangi bir etkiye sahip değildir. + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute yalnızca C# 11 veya sonraki sürümde veya net7.0 veya üzeri hedeflendiğinde geçerlidir. @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - Kısmi '{0}' bildirimlerinin tümü sınıf, tümü kayıt sınıfı, tümü yapı, tümü kayıt yapısı veya tümü arabirim olmalıdır + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + Kısmi '{0}' bildirimlerinin tümü sınıf, tümü kayıt sınıfı, tümü yapı, tümü birleşim, tümü kayıt yapısı veya tümü arabirim olmalıdır diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf index 6babc4e234ab..bd08e50ad518 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hans.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + “{0}”方法的重载未接受 {1} with(...) 元素参数 @@ -387,6 +387,21 @@ 没有显式返回类型的匿名方法不允许使用 AsyncMethodBuilder 属性。 + + Unsafe member '{0}' cannot implement safe member '{1}' + 不安全的成员“{0}”无法实现安全成员“{1}” + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + 不安全的成员“{0}”无法隐式实现安全成员“{1}” + + + + Unsafe member '{0}' cannot override safe member '{1}' + 不安全的成员“{0}”无法覆盖安全成员“{1}” + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. 无法对此成员使用 'OverloadResolutionPriorityAttribute'。 @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + "with(...)" 元素参数不能是动态的 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + 只读接口如果有 "with(...)" 元素,则其必须为空 'with(...)' element must be the first element - 'with(...)' element must be the first element + "with(...)" 元素必须是第一个元素 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + “{0}”类型不支持 "with(...)" 元素 @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - 找不到具有预期签名的可访问“{0}”方法: 具有类型为“ReadOnlySpan<{1}>”且返回类型为“{2}”的单个参数的静态方法。 + 找不到具有预期签名的可访问方法“{0}”: 最后一个参数为“ReadOnlySpan<{1}>”类型且返回类型为“{2}”的静态方法。 @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + 此集合的元素类型不能为 ref 结构或允许 ref 结构的类型参数 + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + 无效的 {0} 值: C# {2} 的“{1}”。请使用语言版本 {3} 或更高版本。 @@ -902,6 +922,11 @@ 表达式树不能包含元组 == 或 != 运算符 + + An expression tree may not contain a union conversion. + 表达式树不能包含联合体转换。 + + An expression tree may not contain a with-expression. 表达式树不能包含 with 表达式。 @@ -1237,6 +1262,16 @@ 不能将内联数组元素字段声明为必需、只读、可变或固定大小缓冲区。 + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + "Union" 声明中不允许显式声明带有单个参数的公共构造函数。 + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + "Union" 声明中不允许包含实例字段、自动属性或类似字段的事件。 + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter “{0}”: 无法使用未命名的接收器参数声明扩展块中的实例成员 @@ -1467,6 +1502,16 @@ “{0}”不包含“{1}”的定义,且找不到任何可访问的扩展方法“{1}”,其中该方法接受类型为“{0}”的第一个参数(是否想用 "await foreach" 来循环访问异步集合?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + 无法通过隐式引用或装箱转换将类型 "{0}" 转换为 "object" + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute 无法应用于此符号。 + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. 目标运行时不支持扩展布局类型。 @@ -1857,11 +1902,6 @@ 应为“警告”、“注释”或指令结束 - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - 无效的 {0} 值: C# {2} 的“{1}”。请使用语言版本 {3} 或更高版本。 - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. 可以为 null 的类型参数必须已知为值类型或不可以为 null 的引用类型,除非使用了语言版本“{0}”或更高版本。请考虑更改语言版本或添加 "class"、"struct" 或类型约束。 @@ -2677,6 +2717,21 @@ 意外的参数列表。 + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + 在 "union" 声明中声明的构造函数必须具有调用合成构造函数或显式声明构造函数的 "this" 初始化表达式。 + + + + A union declaration must specify at least one case type. + 联合体声明必须至少指定一个案例类型。 + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + 此模式无法处理类型为 "{0}" 的表达式,请在此位置查看其他错误。 + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. “{0}”使用 "UnmanagedCallersOnly" 进行特性化,无法直接调用。请获取指向此方法的函数指针。 @@ -2687,11 +2742,36 @@ “{0}”使用 "UnmanagedCallersOnly" 进行特性化,无法转换为委托类型。请获取指向此方法的函数指针。 UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - “{0}”在具有无法识别的 RefSafetyRulesAttribute 版本(应为“11”)的模块中定义。 + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + “{0}”在具有无法识别的 {1} 版本(应为“{2}”)的模块中定义。 + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + 标记为 "RequiresUnsafe" 或 "extern" 的构造函数 "{0}" 需要不安全的上下文以满足 "{2}" 中类型参数 "{1}" 的 "new()" 约束 + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + “{0}”必须在不安全的上下文中使用,因为它标记为 "RequiresUnsafe" 或 "extern" + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + “{0}”必须在不安全的上下文中使用,因为它的签名中包含指针 + + This operation may only be used in an unsafe context + 此操作只能在不安全的上下文中使用 + + + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + 在 SkipLocalsInit 内没有初始值设定项的 stackalloc 表达式只能在不安全的上下文中使用 + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. UnscopedRefAttribute 无法应用于接口实现,因为实现的成员 "{0}" 没有此属性。 @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + 集合表达式参数 @@ -3077,6 +3157,16 @@ nameof 运算符中的未绑定泛型类型 + + unions + 联合体 + + + + updated memory safety rules + 更新后的内存安全规则 + + unsigned right shift 未签名的右移位 @@ -3862,6 +3952,16 @@ 结构成员按引用返回“此”或其他实例成员 + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute 仅在更新后的内存安全规则下有效。 + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute 仅在更新后的内存安全规则下有效。 + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. 返回值必须为非 null,因为参数“{0}”为非 null。 @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4232,7 +4332,7 @@ -errorlog:<file>[,version=<sarif_version>] 指定用于记录所有编译器和分析器诊断的 文件。 - sarif_version:{1|2|2.1} 默认值为 1. 2 和 2.1 + sarif_version:{1|2|2.1} 默认值为 1.2 和 2.1 均表示 SARIF 版本 2.1.0。 -reportanalyzer 报告其他分析器信息,例如 执行时间。 @@ -4274,8 +4374,8 @@ - 高级 - -baseaddress:<address> 要生成的库的基址 -checksumalgorithm:<alg> 指定用于计算存储在 PDB 中的源文件 - 校验和的算法。支持的值为: - SHA1 或 SHA256 (默认值)。 + 校验和的算法。支持的值为: + SHA1、SHA256(默认)、SHA384 或 SHA512。 -codepage:<n> 指定在打开源文件时使用的 代码页 -utf8output 按 UTF-8 编码输出编译器消息 @@ -5855,6 +5955,16 @@ 参数未读。是否忘记通过它来使用该名称初始化属性? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 根据当前内存安全规则,"unsafe" 修饰符在此处没有任何效果。 + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 根据当前内存安全规则,"unsafe" 修饰符在此处没有任何效果。 + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute 仅在 C# 11 或更高版本中或面向 net7.0 或更高版本时有效。 @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - '{0}' 的部分声明必须为所有类、所有记录、所有结构、所有记录结构或所有接口 + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + "{0}" 的部分声明必须为所有类、所有记录类、所有结构、所有联合体、所有记录结构或所有接口 diff --git a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf index d8782450fb63..d029bd1485cc 100644 --- a/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf +++ b/src/Compilers/CSharp/Portable/xlf/CSharpResources.zh-Hant.xlf @@ -234,7 +234,7 @@ No overload for method '{0}' takes {1} 'with(...)' element arguments - No overload for method '{0}' takes {1} 'with(...)' element arguments + 方法 '{0}' 多載不會採用 {1} 'with(...)' 元素引數 @@ -387,6 +387,21 @@ 沒有明確傳回型別的匿名方法上不允許 AsyncMethodBuilder 屬性。 + + Unsafe member '{0}' cannot implement safe member '{1}' + 不安全的成員 '{0}' 無法實作安全成員 '{1}' + + + + Unsafe member '{0}' cannot implicitly implement safe member '{1}' + 不安全的成員 '{0}' 無法隱含地實作安全成員 '{1}' + + + + Unsafe member '{0}' cannot override safe member '{1}' + 不安全的成員 '{0}' 無法覆寫安全成員 '{1}' + + Cannot use 'OverloadResolutionPriorityAttribute' on this member. 無法在此成員上使用 'OverloadResolutionPriorityAttribute'。 @@ -529,22 +544,22 @@ 'with(...)' element arguments cannot be dynamic - 'with(...)' element arguments cannot be dynamic + 'with(...)' 元素引數不能是動態的 'with(...)' element for a read-only interface must be empty if present - 'with(...)' element for a read-only interface must be empty if present + 唯讀介面的 'with(...)' 元素必須是空的 (如果存在) 'with(...)' element must be the first element - 'with(...)' element must be the first element + 'with(...)' 元素必須是第一個元素 'with(...)' elements are not supported for type '{0}' - 'with(...)' elements are not supported for type '{0}' + 類型 '{0}' 不支援 'with(...)' 元素 @@ -559,7 +574,7 @@ Could not find an accessible '{0}' method with the expected signature: a static method whose last parameter is of type 'ReadOnlySpan<{1}>' and return type '{2}'. - 找不到具有預期簽章的可存取 '{0}' 方法:具有類型 'ReadOnlySpan<{1}>' 的單一參數,且傳回類型 '{2}' 的靜態方法。 + 找不到具有預期簽章的可存取 '{0}' 方法:最後一個參數為類型 'ReadOnlySpan<{1}>' 且傳回類型 '{2}' 的靜態方法。 @@ -609,7 +624,12 @@ Element type of this collection may not be a ref struct or a type parameter allowing ref structs - Element type of this collection may not be a ref struct or a type parameter allowing ref structs + 此集合的元素類型不能是 ref struct 或允許 ref struct 的類型參數 + + + + Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. + '{0}' 值無效: 若是 C# {2},則為 '{1}'。請使用 '{3}' 或更高的語言版本。 @@ -902,6 +922,11 @@ 運算式樹狀架構不得包含元組 == 或 != 運算子 + + An expression tree may not contain a union conversion. + 運算式樹狀架構不得包含聯集轉換。 + + An expression tree may not contain a with-expression. 運算式樹狀架構不得包含 with 運算式。 @@ -1237,6 +1262,16 @@ 不可將內嵌陣列元素欄位宣告為必要、readonly、易變或為固定的大小緩衝區。 + + Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + 在 'union' 宣告中,不允許使用單一參數明確宣告的公用建構函式。 + + + + Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + 'union' 宣告中不允許使用執行個體欄位、自動屬性或類似欄位的事件。 + + '{0}': cannot declare instance members in an extension block with an unnamed receiver parameter '{0}': 在擁有未命名接收者參數的擴充區塊中,無法宣告執行個體成員 @@ -1467,6 +1502,16 @@ '{0}' 不包含 '{1}' 的定義,而且找不到接受類型 '{0}' 的第一個引數的可存取方法 '{1}' (您是否要改為使用 'await foreach' 來逐一查看非同步集合?) 'await foreach' is not localizable + + Cannot convert type '{0}' to 'object' via an implicit reference or boxing conversion + 無法透過隱含參考或 Boxing 轉換,將類型 '{0}' 轉換為 'object' + + + + RequiresUnsafeAttribute cannot be applied to this symbol. + RequiresUnsafeAttribute 無法套用至此符號。 + 'RequiresUnsafeAttribute' is not localizable + The target runtime does not support extended layout types. 目標執行階段不支援擴充版面配置型別。 @@ -1857,11 +1902,6 @@ 必須是 'warnings'、'annotations' 或指示詞結尾 - - Invalid '{0}' value: '{1}' for C# {2}. Please use language version '{3}' or greater. - '{0}' 值無效: 若是 C# {2},則為 '{1}'。請使用 '{3}' 或更高的語言版本。 - - A nullable type parameter must be known to be a value type or non-nullable reference type unless language version '{0}' or greater is used. Consider changing the language version or adding a 'class', 'struct', or type constraint. 除非使用語言版本 '{0}' 或更新版本,否則就必須知道可為 Null 的型別參數是實值型別還是不可為 Null 的參考型別。請考慮變更語言版本,或新增 'class'、'struct' 或類型條件約束。 @@ -2677,6 +2717,21 @@ 未預期的參數清單。 + + A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + 在 'union' 宣告中宣告的建構函式必須具有 'this' 初始設定式,以呼叫合成的建構函式或明確宣告的建構函式。 + + + + A union declaration must specify at least one case type. + 聯集宣告必須指定至少一個案例類型。 + + + + An expression of type '{0}' cannot be handled by this pattern, see additional errors at this location. + 此模式無法處理 '{0}' 類型的運算式,請在此位置查看其他錯誤。 + + '{0}' is attributed with 'UnmanagedCallersOnly' and cannot be called directly. Obtain a function pointer to this method. '{0}' 使用 'UnmanagedCallersOnly' 屬性化,因此無法直接呼叫。取得此方法的函式指標。 @@ -2687,11 +2742,36 @@ '{0}' 使用 'UnmanagedCallersOnly' 屬性化,因此無法轉換為委派類型。取得此方法的函式指標。 UnmanagedCallersOnly is not localizable. - - '{0}' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. - '{0}' 是在具有無法辨識的 RefSafetyRulesAttribute 版本的模組中定義,預期為 '11'。 + + '{0}' is defined in a module with an unrecognized {1} version, expecting '{2}'. + '{0}' 定義於具有無法辨識 {1} 版本的模組中,預期為 '{2}'。 + + + + An unsafe context is required for constructor '{0}' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter '{1}' in '{2}' + 建構函式 '{0}' 標記為 'RequiresUnsafe' 或 'extern',需要不安全的上下文,以符合 '{1}' 中類型參數 '{2}' 的 'new()' 限制 + 'RequiresUnsafe', 'extern', and 'new()' should not be localized. {2} is the generic method or type. + + + '{0}' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + '{0}' 必須在不安全的內容中使用,因為它被標示為 'RequiresUnsafe' 或 'extern' + 'RequiresUnsafe' and 'extern' should not be localized. + + + '{0}' must be used in an unsafe context because it has pointers in its signature + '{0}' 必須在不安全的內容中使用,因為它的簽章中有指標 + + + + This operation may only be used in an unsafe context + 此作業只能在不安全的內容中使用 + + stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + SkipLocalsInit 內沒有初始設定式的 stackalloc 運算式只能在不安全的內容中使用 + 'stackalloc' and 'SkipLocalsInit' are identifiers, should not be localized. + UnscopedRefAttribute cannot be applied to an interface implementation because implemented member '{0}' doesn't have this attribute. UnscopedRefAttribute 無法套用至介面實作,因為實作的成員 '{0}' 不具這項屬性。 @@ -2849,7 +2929,7 @@ collection expression arguments - collection expression arguments + 集合運算式引數 @@ -3077,6 +3157,16 @@ 運算子 nameof 中的未系結泛型型別 + + unions + 聯集 + + + + updated memory safety rules + 已更新記憶體安全規則 + + unsigned right shift 未簽署右移位 @@ -3862,6 +3952,16 @@ 結構成員藉傳址方式傳回 'this' 或其他執行個體成員 + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute 只有在更新的記憶體安全規則下才有效。 + 'RequiresUnsafeAttribute' is not localizable + + + RequiresUnsafeAttribute is only valid under the updated memory safety rules. + RequiresUnsafeAttribute 只有在更新的記憶體安全規則下才有效。 + 'RequiresUnsafeAttribute' is not localizable + Return value must be non-null because parameter '{0}' is non-null. 因為參數 '{0}' 不是 null,所以傳回值必須非 null。 @@ -4115,7 +4215,7 @@ -baseaddress:<address> Base address for the library to be built -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<n> Specify the codepage to use when opening source files -utf8output Output compiler messages in UTF-8 encoding @@ -4154,164 +4254,164 @@ accessibility errors with what assembly they came from. - Visual C# Compiler Options + Visual C# 編譯器選項 - OUTPUT FILES - --out:<file> Specify output file name (default: base name of - file with main class or first file) --target:exe Build a console executable (default) (Short - form: -t:exe) --target:winexe Build a Windows executable (Short form: +-out:<file> 指定輸出檔案名稱 (預設: + 具有主要類別或第一個檔案的檔案基礎名稱) +-target:exe 建置主控台可執行檔 (預設) (簡短 + 形式: -t:exe) +-target:winexe 建置 Windows 可執行檔 (簡短形式: -t:winexe) --target:library Build a library (Short form: -t:library) --target:module Build a module that can be added to another - assembly (Short form: -t:module) --target:appcontainerexe Build an Appcontainer executable (Short form: +-target:library 建置程式庫 (簡短形式: -t:library) +-target:module 建置可以新增至其他 + 組件的模組 (簡短形式: -t:module) +-target:appcontainerexe 建置 Appcontainer 可執行檔 (簡短形式: -t:appcontainerexe) --target:winmdobj Build a Windows Runtime intermediate file that - is consumed by WinMDExp (Short form: -t:winmdobj) --doc:<file> XML Documentation file to generate --refout:<file> Reference assembly output to generate --platform:<string> Limit which platforms this code can run on: x86, - Itanium, x64, arm, arm64, anycpu32bitpreferred, or - anycpu. The default is anycpu. +-target:winmdobj 建置由 WinMDExp 取用 + 的 Windows 執行階段中繼檔案 (簡短形式: -t:winmdobj) +-doc:<file> 要產生的 XML 文件檔案 +-refout:<file> 要產生的參考組件輸出 +-platform:<string> 限制此程式碼可在哪些平台上執行: x86、 + Itanium、x64、arm、arm64、anycpu32bitpreferred 或 + anycpu。預設為 anycpu。 - INPUT FILES - --recurse:<wildcard> Include all files in the current directory and - subdirectories according to the wildcard - specifications --reference:<alias>=<file> Reference metadata from the specified assembly - file using the given alias (Short form: -r) --reference:<file list> Reference metadata from the specified assembly - files (Short form: -r) --addmodule:<file list> Link the specified modules into this assembly --link:<file list> Embed metadata from the specified interop - assembly files (Short form: -l) --analyzer:<file list> Run the analyzers from this assembly - (Short form: -a) --additionalfile:<file list> Additional files that don't directly affect code - generation but may be used by analyzers for producing - errors or warnings. --embed Embed all source files in the PDB. --embed:<file list> Embed specific files in the PDB. +-recurse:<wildcard> 根據萬用字元 + 規格 + 包含目前目錄和子目錄中的所有檔案 +-reference:<alias>=<file> 從指定的組件 + 檔使用指定的別名來參考中繼資料 (簡短形式: -r) +-reference:<file list> 從指定的組件檔 + 來參考中繼資料 (簡短形式: -r) +-addmodule:<file list> 將指定的模組連結至此組件中 +-link:<file list> 從指定的 Interop + 組件檔來內嵌中繼資料 (簡短形式: -l) +-analyzer:<file list> 從此組件執行分析器 + (簡短形式: -a) +-additionalfile:<file list> 不會直接影響程式碼 + 產生但可由分析器用來產生錯誤或警告 + 的其他檔案。 +-embed 在 PDB 中内嵌所有來源檔案。 +-embed:<file list> 在 PDB 中内嵌特定檔案。 - RESOURCES - --win32res:<file> Specify a Win32 resource file (.res) --win32icon:<file> Use this icon for the output --win32manifest:<file> Specify a Win32 manifest file (.xml) --nowin32manifest Do not include the default Win32 manifest --resource:<resinfo> Embed the specified resource (Short form: -res) --linkresource:<resinfo> Link the specified resource to this assembly - (Short form: -linkres) Where the resinfo format - is <file>[,<string name>[,public|private]] +-win32res:<file> 指定 Win32 資源檔 (.res) +-win32icon:<file> 使用此圖示來進行輸出 +-win32manifest:<file> 指定 Win32 資訊清單檔 (.xml) +-nowin32manifest 不要包含預設的 Win32 資訊清單 +-resource:<resinfo> 嵌入指定的資源 (簡短形式: -res) +-linkresource:<resinfo> 將指定的資源連結到此組件 + (簡短形式: -linkres) 其中 resinfo 格式 + 為 <file>[,<string name>[,public|private]] - CODE GENERATION - --debug[+|-] Emit debugging information +-debug[+|-] 發出偵錯資訊 -debug:{full|pdbonly|portable|embedded} - Specify debugging type ('full' is default, - 'portable' is a cross-platform format, - 'embedded' is a cross-platform format embedded into - the target .dll or .exe) --optimize[+|-] Enable optimizations (Short form: -o) --deterministic Produce a deterministic assembly - (including module version GUID and timestamp) --refonly Produce a reference assembly in place of the main output --instrument:TestCoverage Produce an assembly instrumented to collect - coverage information --sourcelink:<file> Source link info to embed into PDB. + 指定偵錯類型 ('full' 為預設, + 'portable' 是跨平台格式, + 'embedded' 是內嵌至 + 目標 .dll 或 .exe 中的跨平台格式) +-optimize[+|-] 啟用最佳化 (簡短形式: -o) +-deterministic 產生具決定性組件 + (包括模組版本 GUID 和時間戳記) +-refonly 產生參考組件,以取代主要輸出 +strument:TestCoverage 產生檢測要收集 + 涵蓋範圍資訊的組件 +-sourcelink:<file> 要內嵌至 PDB 中的來源連結資訊。 - ERRORS AND WARNINGS - --warnaserror[+|-] Report all warnings as errors --warnaserror[+|-]:<warn list> Report specific warnings as errors - (use "nullable" for all nullability warnings) --warn:<n> Set warning level (0 or higher) (Short form: -w) --nowarn:<warn list> Disable specific warning messages - (use "nullable" for all nullability warnings) --ruleset:<file> Specify a ruleset file that disables specific - diagnostics. +-warnaserror[+|-] 將所有警告回報為錯誤 +-warnaserror[+|-]:<warn list> 將特定的警告回報為錯誤 + (針對所有可為 Null 的警告使用 "nullable") +-warn:<n> 設定警告層級 (0 或更高) (簡短形式: -w) +-nowarn:<warn list> 停用特定的警告訊息 + (針對所有可為 Null 的警告使用 "nullable") +-ruleset:<file> 指定會停用特定 + 診斷的檔案。 -errorlog:<file>[,version=<sarif_version>] - Specify a file to log all compiler and analyzer - diagnostics. - sarif_version:{1|2|2.1} Default is 1. 2 and 2.1 - both mean SARIF version 2.1.0. --reportanalyzer Report additional analyzer information, such as - execution time. --skipanalyzers[+|-] Skip execution of diagnostic analyzers. + 指定用來記錄所有編譯器和分析器 + 診斷的檔案。 + sarif_version:{1|2|2.1} 預設為1. 2 和 2.1, + 都表示 SARIF 版本 2.1.0。 +-reportanalyzer 回報其他分析器資訊,例如 + 執行時間。 +-skipanalyzers[+|-] 略過診斷分析器的執行。 - LANGUAGE - --checked[+|-] Generate overflow checks --unsafe[+|-] Allow 'unsafe' code --define:<symbol list> Define conditional compilation symbol(s) (Short - form: -d) --langversion:? Display the allowed values for language version --langversion:<string> Specify language version such as - `latest` (latest version, including minor versions), - `default` (same as `latest`), - `latestmajor` (latest version, excluding minor versions), - `preview` (latest version, including features in unsupported preview), - or specific versions like `6` or `7.1` --nullable[+|-] Specify nullable context option enable|disable. +-checked[+|-] 產生溢位檢查 +-unsafe[+|-] 允許 'unsafe' 程式碼 +-define:<symbol list> 定義條件式編譯符號 (簡短 + 形式: -d) +-langversion:? 顯示語言版本的允許值 +-langversion:<string> 指定語言版本,例如 + `latest` (最新版本,包括次要版本), + `default` (與 `latest` 相同), + `latestmajor` (最新版本,排除次要版本), + `preview` (最新版本,包括不支援預覽中的功能), + 或特定版本,例如 `6` 或 `7.1` +-nullable[+|-] 指定可為 Null 內容選項啟用|停用。 -nullable:{enable|disable|warnings|annotations} - Specify nullable context option enable|disable|warnings|annotations. + 指定可為 Null 內容選項啟用|停用|警告|註釋。 - SECURITY - --delaysign[+|-] Delay-sign the assembly using only the public - portion of the strong name key --publicsign[+|-] Public-sign the assembly using only the public - portion of the strong name key --keyfile:<file> Specify a strong name key file --keycontainer:<string> Specify a strong name key container --highentropyva[+|-] Enable high-entropy ASLR +-delaysign[+|-] 只使用強式名稱金鑰的公開 + 部分對組件進行延遲簽屬 +-publicsign[+|-] 只使用強式名稱金鑰的公開 + 部分對組件進行公開簽屬 +-keyfile:<file> 指定強式名稱金鑰檔案 +-keycontainer:<string> 指定強式名稱金鑰容器 +-highentropyva[+|-] 啟用高熵 ASLR - MISCELLANEOUS - -@<file> Read response file for more options --help Display this usage message (Short form: -?) --nologo Suppress compiler copyright message --noconfig Do not auto include CSC.RSP file --parallel[+|-] Concurrent build. --version Display the compiler version number and exit. +@<file> 讀取回應檔以取得更多選項 +-help 顯示此使用方式訊息 (簡短形式 form: -?) +-nologo 隱藏編譯器著作權訊息 +-noconfig 不要自動包括 CSC.RSP 檔案 +-parallel[+|-] 同時建置。 +-version 顯示編譯器版本號碼並結束。 - ADVANCED - --baseaddress:<address> Base address for the library to be built --checksumalgorithm:<alg> Specify algorithm for calculating source file - checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). --codepage:<n> Specify the codepage to use when opening source - files --utf8output Output compiler messages in UTF-8 encoding --main:<type> Specify the type that contains the entry point - (ignore all other possible entry points) (Short - form: -m) --fullpaths Compiler generates fully qualified paths --filealign:<n> Specify the alignment used for output file - sections +-baseaddress:<address> 要建置程式庫的基底位址 +-checksumalgorithm:<alg> 指定計算儲存在 PDB 中 + 來源檔案總和檢查碼的演算法。支援的值為: + SHA1、SHA256 (預設)、SHA384 或 SHA512。 +-codepage:<n> 指定開啟來源 + 檔案時所要使用的字碼頁 +-utf8output 輸出編譯器訊息 (以 UTF-8 編碼) +-main:<type> 指定包含進入點的類型 + (略過所有其他可能的進入點) (簡短 + 形式: -m) +-fullpaths 編譯器會產生完整路徑 +-filealign:<n> 指定用於輸出檔案 + 區段的對齊 -pathmap:<K1>=<V1>,<K2>=<V2>,... - Specify a mapping for source path names output by - the compiler. --pdb:<file> Specify debug information file name (default: - output file name with .pdb extension) --errorendlocation Output line and column of the end location of - each error --preferreduilang Specify the preferred output language name. --nosdkpath Disable searching the default SDK path for standard library assemblies. --sdkpath:<path> Path used to search for standard library assemblies. --nostdlib[+|-] Do not reference standard library (mscorlib.dll) --subsystemversion:<string> Specify subsystem version of this assembly --lib:<file list> Specify additional directories to search in for - references --errorreport:<string> Specify how to handle internal compiler errors: - prompt, send, queue, or none. The default is - queue. --appconfig:<file> Specify an application configuration file - containing assembly binding settings --moduleassemblyname:<string> Name of the assembly which this module will be - a part of --modulename:<string> Specify the name of the source module --generatedfilesout:<dir> Place files generated during compilation in the - specified directory. --reportivts[+|-] Output information on all IVTs granted to this - assembly by all dependencies, and annotate foreign assembly - accessibility errors with what assembly they came from. + 指定編譯器的來源路徑名稱輸出的對應 + 。 +-pdb:<file> 指定偵錯資訊檔案名稱 (預設: + 具有 .pdb 副檔名的輸出檔案名稱) +-errorendlocation 輸出每個錯誤 + 行與資料行的結束位置 +-preferreduilang 指定喜好的輸出語言名稱。 +-nosdkpath 停用搜尋標準程式庫組件的預設 SDK 路徑。 +-sdkpath:<path> 用來搜尋標準文件庫組件的路徑。 +-nostdlib[+|-] 不參考標準程式庫 (mscorlib.dll) +-subsystemversion:<string> 指定此組件的子系統版本 +-lib:<file list> 指定要在其中搜尋的其他目錄以作為 + 參考 +-errorreport:<string> 指定如何處理內部編譯器錯誤: + 提示、傳送、佇列或無。預設為 + 佇列。 +-appconfig:<file> 指定包含組件繫結設定的 + 應用程式設定檔 +-moduleassemblyname:<string> 此模組將成為其一部分 + 的組件名稱 +-modulename:<string> 指定來源模組的名稱 +-generatedfilesout:<dir> 將編譯期間產生的檔案放在 + 指定的目錄。 +-reportivts[+|-] 輸出有關所有相依項授與至此 + 組件的所有 IVT 的資訊,並使用它們來自的組件 + 標註外部組件的可存取性錯誤。 Visual C# Compiler Options @@ -5855,6 +5955,16 @@ 參數未讀取。是否忘記使用該參數來初始化該名稱的屬性? + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 根據目前的記憶體安全規則,'unsafe' 修飾元在此不會產生任何效果。 + 'unsafe' is a keyword, should not be localized. + + + The 'unsafe' modifier does not have any effect here under the current memory safety rules. + 根據目前的記憶體安全規則,'unsafe' 修飾元在此不會產生任何效果。 + 'unsafe' is a keyword, should not be localized. + UnscopedRefAttribute is only valid in C# 11 or later or when targeting net7.0 or later. UnscopedRefAttribute 只在 C# 11 或更新版本中有效,或以 net7.0 或更新版本為目標時有效。 @@ -7131,8 +7241,8 @@ - Partial declarations of '{0}' must be all classes, all record classes, all structs, all record structs, or all interfaces - '{0}' 中有一部分宣告必須全是類別、全是記錄類別、全是結構、全是記錄結構,或全是介面 + Partial declarations of '{0}' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + '{0}' 中有一部分宣告必須全是類別、全是記錄類別、全是結構、全是聯集、全是記錄結構,或全是介面 diff --git a/src/Compilers/CSharp/Test/CSharp15/ClosedClassesTests.cs b/src/Compilers/CSharp/Test/CSharp15/ClosedClassesTests.cs index a0c77a9066ec..aaedc5db88cf 100644 --- a/src/Compilers/CSharp/Test/CSharp15/ClosedClassesTests.cs +++ b/src/Compilers/CSharp/Test/CSharp15/ClosedClassesTests.cs @@ -210,7 +210,7 @@ sealed closed class C { } var comp = CreateCompilation([source, ClosedAttributeDefinition], targetFramework: TargetFramework.Net100); comp.VerifyEmitDiagnostics( - // (1,21): error CS9366: 'C': a closed type cannot be sealed or static + // (1,21): error CS9601: 'C': a closed type cannot be sealed or static // sealed closed class C { } Diagnostic(ErrorCode.ERR_ClosedSealedStatic, "C").WithArguments("C").WithLocation(1, 21)); @@ -229,7 +229,7 @@ static closed class C { } var comp = CreateCompilation([source, ClosedAttributeDefinition], targetFramework: TargetFramework.Net100); comp.VerifyEmitDiagnostics( - // (1,21): error CS9366: 'C': a closed type cannot be sealed or static + // (1,21): error CS9601: 'C': a closed type cannot be sealed or static // static closed class C { } Diagnostic(ErrorCode.ERR_ClosedSealedStatic, "C").WithArguments("C").WithLocation(1, 21)); @@ -420,7 +420,7 @@ public class D : C { } """; var comp2 = CreateCompilation(source2, references: [reference], targetFramework: TargetFramework.Net100); comp2.VerifyEmitDiagnostics( - // (1,14): error CS9367: 'D': cannot use a closed type 'C' from another assembly as a base type. + // (1,14): error CS9602: 'D': cannot use a closed type 'C' from another assembly as a base type. // public class D : C { } Diagnostic(ErrorCode.ERR_ClosedBaseTypeBaseFromOtherAssembly, "D").WithArguments("D", "C").WithLocation(1, 14)); } @@ -441,7 +441,7 @@ public class D : C { } """; var comp2 = CreateCompilation(source2, references: [comp1.EmitToImageReference()], targetFramework: TargetFramework.Net100); comp2.VerifyEmitDiagnostics( - // (1,14): error CS9367: 'D': cannot use a closed type 'C' from another assembly as a base type. + // (1,14): error CS9602: 'D': cannot use a closed type 'C' from another assembly as a base type. // public class D : C { } Diagnostic(ErrorCode.ERR_ClosedBaseTypeBaseFromOtherAssembly, "D").WithArguments("D", "C").WithLocation(1, 14)); } @@ -512,7 +512,7 @@ public class E : D { } """; var comp2 = CreateCompilation(source2, references: [reference], targetFramework: TargetFramework.Net100); comp2.VerifyEmitDiagnostics( - // (1,14): error CS9367: 'E': cannot use a closed type 'D' from another assembly as a base type. + // (1,14): error CS9602: 'E': cannot use a closed type 'D' from another assembly as a base type. // public class E : D { } Diagnostic(ErrorCode.ERR_ClosedBaseTypeBaseFromOtherAssembly, "E").WithArguments("E", "D").WithLocation(1, 14)); } @@ -582,7 +582,7 @@ public class D : C { } """; var comp1 = CreateCompilation([source1, ClosedAttributeDefinition], targetFramework: TargetFramework.Net100); comp1.VerifyEmitDiagnostics( - // (2,14): error CS9368: 'D': The type parameter 'T' must be referenced in the base type 'C' because the base type is closed. + // (2,14): error CS9603: 'D': The type parameter 'T' must be referenced in the base type 'C' because the base type is closed. // public class D : C { } Diagnostic(ErrorCode.ERR_UnderspecifiedClosedSubtype, "D").WithArguments("D", "T", "C").WithLocation(2, 14)); } @@ -617,7 +617,7 @@ public class D : C { } """; var comp1 = CreateCompilation([source1, ClosedAttributeDefinition], targetFramework: TargetFramework.Net100); comp1.VerifyEmitDiagnostics( - // (5,18): error CS9368: 'Outer.D': The type parameter 'T' must be referenced in the base type 'C' because the base type is closed. + // (5,18): error CS9603: 'Outer.D': The type parameter 'T' must be referenced in the base type 'C' because the base type is closed. // public class D : C { } Diagnostic(ErrorCode.ERR_UnderspecifiedClosedSubtype, "D").WithArguments("Outer.D", "T", "C").WithLocation(5, 18)); } @@ -648,10 +648,10 @@ class D : C { } """; var comp1 = CreateCompilation([source1, ClosedAttributeDefinition], targetFramework: TargetFramework.Net100); comp1.VerifyEmitDiagnostics( - // (4,11): error CS9368: 'Outer.D': The type parameter 'U5' must be referenced in the base type 'C' because the base type is closed. + // (4,11): error CS9603: 'Outer.D': The type parameter 'U5' must be referenced in the base type 'C' because the base type is closed. // class D : C { } Diagnostic(ErrorCode.ERR_UnderspecifiedClosedSubtype, "D").WithArguments("Outer.D", "U5", "C").WithLocation(4, 11), - // (4,11): error CS9368: 'Outer.D': The type parameter 'U3' must be referenced in the base type 'C' because the base type is closed. + // (4,11): error CS9603: 'Outer.D': The type parameter 'U3' must be referenced in the base type 'C' because the base type is closed. // class D : C { } Diagnostic(ErrorCode.ERR_UnderspecifiedClosedSubtype, "D").WithArguments("Outer.D", "U3", "C").WithLocation(4, 11)); } @@ -669,7 +669,7 @@ public closed class E { } """; var comp1 = CreateCompilation([source1, ClosedAttributeDefinition], targetFramework: TargetFramework.Net100); comp1.VerifyEmitDiagnostics( - // (5,14): error CS9368: 'F': The type parameter 'T' must be referenced in the base type 'E' because the base type is closed. + // (5,14): error CS9603: 'F': The type parameter 'T' must be referenced in the base type 'E' because the base type is closed. // closed class F : E { } Diagnostic(ErrorCode.ERR_UnderspecifiedClosedSubtype, "F").WithArguments("F", "T", "E").WithLocation(5, 14)); } @@ -939,13 +939,13 @@ public void M(C c) } """, references: [reference], targetFramework: TargetFramework.Net100); comp2.VerifyEmitDiagnostics( - // (3,7): error CS9367: 'D': cannot use a closed type 'C' from another assembly as a base type. + // (3,7): error CS9602: 'D': cannot use a closed type 'C' from another assembly as a base type. // class D : C { } Diagnostic(ErrorCode.ERR_ClosedBaseTypeBaseFromOtherAssembly, "D").WithArguments("D", "C").WithLocation(3, 7), - // (4,7): error CS9367: 'E': cannot use a closed type 'C' from another assembly as a base type. + // (4,7): error CS9602: 'E': cannot use a closed type 'C' from another assembly as a base type. // class E() : C { } Diagnostic(ErrorCode.ERR_ClosedBaseTypeBaseFromOtherAssembly, "E").WithArguments("E", "C").WithLocation(4, 7), - // (5,7): error CS9367: 'F': cannot use a closed type 'C' from another assembly as a base type. + // (5,7): error CS9602: 'F': cannot use a closed type 'C' from another assembly as a base type. // class F : C Diagnostic(ErrorCode.ERR_ClosedBaseTypeBaseFromOtherAssembly, "F").WithArguments("F", "C").WithLocation(5, 7), // (16,17): error CS9035: Required member 'C.P' must be set in the object initializer or attribute constructor. diff --git a/src/Compilers/CSharp/Test/CSharp15/UnionsTests.cs b/src/Compilers/CSharp/Test/CSharp15/UnionsTests.cs new file mode 100644 index 000000000000..faf517593f3b --- /dev/null +++ b/src/Compilers/CSharp/Test/CSharp15/UnionsTests.cs @@ -0,0 +1,24958 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable disable + +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests +{ + public class UnionsTests : CSharpTestBase + { + [Fact] + public void UnionType_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +public interface IUnion +{ +#nullable enable + object? Value { get; } +#nullable disable +} + +[System.Runtime.CompilerServices.Union] +interface I1; + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public object Value => null; +} + +struct S2 : IUnion +{ + public object Value => null; +} + +[System.Runtime.CompilerServices.Union] +class C1 +{ + public object Value => null; +} + +[System.Runtime.CompilerServices.Union] +sealed class C2 +{ + public object Value => null; +} + +sealed class C3 : IUnion +{ + public object Value => null; +} + +sealed class C4 : C1 +{ +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyEmitDiagnostics(); + + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + Assert.True(comp.GetTypeByMetadataName("C1").IsUnionType); + Assert.True(comp.GetTypeByMetadataName("C2").IsUnionType); + Assert.False(comp.GetTypeByMetadataName("C4").IsUnionType); + + Assert.False(comp.GetTypeByMetadataName("I1").IsUnionType); + Assert.False(comp.GetTypeByMetadataName("S2").IsUnionType); + Assert.False(comp.GetTypeByMetadataName("C3").IsUnionType); + } + + [Fact] + public void UnionType_02_UnionNotPublic() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public object Value => null; +} + +namespace System.Runtime.CompilerServices +{ + public class UnionAttribute : System.Attribute + { + } +} +"; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics(); + + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + } + + [Fact] + public void UnionType_03_ManyUnionAttributeTypes() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +public struct S1 +{ + public object Value => null; +} +"; + var comp1 = CreateCompilation([src1, UnionAttributeSource]); + comp1.VerifyEmitDiagnostics(); + Assert.True(comp1.GetTypeByMetadataName("S1").IsUnionType); + + var src2 = @" +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public object Value => null; +} +"; + var comp2 = CreateCompilation([src2, UnionAttributeSource], references: [comp1.EmitToImageReference()]); + comp1.VerifyEmitDiagnostics(); + + Assert.True(comp2.GetTypeByMetadataName("S1").IsUnionType); + Assert.True(comp2.GetTypeByMetadataName("S2").IsUnionType); + } + + [Fact] + public void CaseTypes_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public object Value => null; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x){} + public S2(string x){} + public object Value => null; +} + +[System.Runtime.CompilerServices.Union] +struct S3 +{ + private S3(int x){} + internal S3(string x){} + public object Value => null; +} + +[System.Runtime.CompilerServices.Union] +struct S4 +{ + public S4(int x, string y){} + public object Value => null; +} + +[System.Runtime.CompilerServices.Union] +class C5 +{ + protected C5(int x){} + protected internal C5(string x){} + private protected C5(decimal x){} + public object Value => null; +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyEmitDiagnostics(); + + VerifyCaseTypes(comp, "S1", []); + VerifyCaseTypes(comp, "S2", ["System.Int32", "System.String"]); + VerifyCaseTypes(comp, "S3", []); + VerifyCaseTypes(comp, "S4", []); + VerifyCaseTypes(comp, "C5", []); + } + + private static void VerifyCaseTypes(CSharpCompilation comp, string typeName, string[] caseTypes) + { + var type = comp.GetTypeByMetadataName(typeName); + Assert.True(type.IsUnionType); + AssertEx.SequenceEqual(caseTypes, type.UnionCaseTypes.ToTestDisplayStrings()); + } + + [Fact] + public void CaseTypes_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S2 +{ +#line 4 + public static S2(int x){} + public object Value => null; +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (4,19): error CS0515: 'S2.S2(int)': access modifiers are not allowed on static constructors + // public static S2(int x){} + Diagnostic(ErrorCode.ERR_StaticConstructorWithAccessModifiers, "S2").WithArguments("S2.S2(int)").WithLocation(4, 19), + // (4,19): error CS0132: 'S2.S2(int)': a static constructor must be parameterless + // public static S2(int x){} + Diagnostic(ErrorCode.ERR_StaticConstParam, "S2").WithArguments("S2.S2(int)").WithLocation(4, 19) + ); + + VerifyCaseTypes(comp, "S2", []); + } + + [Fact] + public void CaseTypes_03() + { + var src = @" +struct S2 +{ + public S2(int x){} + public S2(string x){} + public object Value => null; +} +"; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics(); + + var type = comp.GetTypeByMetadataName("S2"); + Assert.False(type.IsUnionType); + AssertEx.SequenceEqual([], type.UnionCaseTypes.ToTestDisplayStrings()); + } + + [Fact] + public void CaseTypes_04() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class C1 +{ + public C1(int x){} + public object Value => null; +} + +[System.Runtime.CompilerServices.Union] +sealed class C2 : C1 +{ + public C2(string x) : base(0) {} + public new object Value => null; +} + +class C3 +{ + public C3(int x){} +} + +[System.Runtime.CompilerServices.Union] +sealed class C4 : C3 +{ + public C4(string x) : base(0) {} + public object Value => null; +} + +sealed class C5 : C1 +{ + public C5(string x) : base(0) {} +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyEmitDiagnostics(); + + VerifyCaseTypes(comp, "C1", ["System.Int32"]); + VerifyCaseTypes(comp, "C2", ["System.String"]); + VerifyCaseTypes(comp, "C4", ["System.String"]); + + var c5 = comp.GetTypeByMetadataName("C5"); + Assert.False(c5.IsUnionType); + AssertEx.SequenceEqual([], c5.UnionCaseTypes.ToTestDisplayStrings()); + } + + [Fact] + public void CaseTypes_05() + { + var src = @" +#nullable enable +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(string?[] x){} +#line 6 + public S2(string[] x){} + public S2((int a, int b) x){} + public S2((int, int) x){} + + public object Value => null!; +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (6,12): error CS0111: Type 'S2' already defines a member called 'S2' with the same parameter types + // public S2(string? x){} + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "S2").WithArguments("S2", "S2").WithLocation(6, 12), + // (8,12): error CS0111: Type 'S2' already defines a member called 'S2' with the same parameter types + // public S2((int, int) x){} + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "S2").WithArguments("S2", "S2").WithLocation(8, 12) + ); + + VerifyCaseTypes(comp, "S2", ["System.String?[]", "(System.Int32 a, System.Int32 b)"]); + } + + [Fact] + public void CaseTypes_06() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int? x){} + public S1(string? x){} + public object? Value => null; +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyEmitDiagnostics(); + + VerifyCaseTypes(comp, "S1", ["System.Int32", "System.String"]); + } + + [Fact] + public void UnionMatching_01_Discard_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + public S1() { Value = null; } + public S1(int x) { Value = x; } + public S1(string x) { Value = x; } + public object Value { get; } +} + +class Program +{ + static void Main() + { + System.Console.Write(Test(new S1(10))); + System.Console.Write(Test(null)); + System.Console.Write(Test(new S1())); + } + + static bool Test(S1 u) + { + if (u switch {_ => true }) + { + return true; + } + + return false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueTrueTrue").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldc.i4.1 + IL_0001: brfalse.s IL_0005 + IL_0003: ldc.i4.1 + IL_0004: ret + IL_0005: ldc.i4.0 + IL_0006: ret +} +"); + } + + [Fact] + public void UnionMatching_01_Discard_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1() { Value = null; } + public S1(int x) { Value = x; } + public S1(string x) { Value = x; } + public object Value { get; } +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1())); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1 u) + { + if (u switch {_ => true }) + { + return true; + } + + return false; + } + + static bool Test2(S1? u) + { + if (u switch {_ => true }) + { + return true; + } + + return false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueTrueTrue TrueTrueTrueTrue").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldc.i4.1 + IL_0001: brfalse.s IL_0005 + IL_0003: ldc.i4.1 + IL_0004: ret + IL_0005: ldc.i4.0 + IL_0006: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldc.i4.1 + IL_0001: brfalse.s IL_0005 + IL_0003: ldc.i4.1 + IL_0004: ret + IL_0005: ldc.i4.0 + IL_0006: ret +} +"); + } + + [Fact] + public void UnionMatching_02_Var_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) { Value = x; } + public S1(string x) { Value = x; } + public object Value { get; } + + public int Int => 123; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(null) is null); + } + + static int Test1(S1 u) + { + return (u switch {var v => v }).Int; + } + + static int? Test2(S1? u) + { + return (u switch {var v => v })?.Int; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "123123 123123True").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_02_Var_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + public S1(int x) { Value = x; } + public S1(string x) { Value = x; } + public object Value { get; } + + public int Int => 123; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(null) is null); + } + + static int? Test1(S1 u) + { + return (u switch {var v => v })?.Int; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "123True").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_03_Var_Deconstruct_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) { Value = x; } + public S1(string x) { Value = x; } + public object Value { get; } + + public void Deconstruct(out int x, out int y) => throw null; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(null)); + } + + static int Test1(S1 u) + { + return (u switch {var (a, b) => a * 1000 + b * 10, _ => -1 } ); + } + + static int Test2(S1? u) + { + return (u switch {var (a, b) => a * 1000 + b * 10, _ => -1 } ); + } +} + +static class Extensions +{ + public static void Deconstruct(this object o, out int x, out int y) + { + x = 1; + y = 2; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "1020-1 1020-1-1").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_03_Var_Deconstruct_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + public S1(int x) { Value = x; } + public S1(string x) { Value = x; } + public object Value { get; } + + public void Deconstruct(out int x, out int y) => throw null; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(null)); + } + + static int Test2(S1 u) + { + return (u switch {var (a, b) => a * 1000 + b * 10, _ => -1 } ); + } +} + +static class Extensions +{ + public static void Deconstruct(this object o, out int x, out int y) + { + x = 1; + y = 2; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "1020-1").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_04_Var_ITuple_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(new C()))); + + System.Console.Write(' '); + System.Console.Write(Test2((new S1(10), -1))); + System.Console.Write(Test2((default, -1))); + System.Console.Write(Test2((new S1(new C()), -1))); + + System.Console.Write(' '); + System.Console.Write(Test3(new C2(new S1(10)))); + System.Console.Write(Test3(new C2(default))); + System.Console.Write(Test3(new C2(new S1(new C())))); + } + + static bool Test1(S1 u) + { + return u is var (_, i) && (int)i == 10; + } + + static bool Test2((S1, int) u) + { + return u is var ((_, i), _) && (int)i == 10; + } + + static bool Test3(C2 u) + { + return u is var (_, ((_, i), _, _)) && (int)i == 10; + } +} + +public class C : System.Runtime.CompilerServices.ITuple +{ + int System.Runtime.CompilerServices.ITuple.Length => 2; + object System.Runtime.CompilerServices.ITuple.this[int i] => i * 10; +} + +class C2 : System.Runtime.CompilerServices.ITuple +{ + private readonly S1 _value; + public C2(S1 x) { _value = x; } + int System.Runtime.CompilerServices.ITuple.Length => 2; + object System.Runtime.CompilerServices.ITuple.this[int i] => _value; +} + +static class Extensions +{ + public static void Deconstruct(this object o, out S1 x, out int y, out int z) + { + x = (S1)o; + y = 2; + z = 3; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "FalseFalseTrue FalseFalseTrue FalseFalseTrue" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_04_Var_ITuple_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(new C()))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2((new S1(10), -1))); + System.Console.Write(Test2((default(S1), -1))); + System.Console.Write(Test2((new S1(new C()), -1))); + System.Console.Write(Test2((null, -1))); + + System.Console.Write(' '); + System.Console.Write(Test3(new C2(new S1(10)))); + System.Console.Write(Test3(new C2(default(S1)))); + System.Console.Write(Test3(new C2(new S1(new C())))); + System.Console.Write(Test3(new C2(null))); + } + + static bool Test1(S1? u) + { + return u is var (_, i) && (int)i == 10; + } + + static bool Test2((S1?, int) u) + { + return u is var ((_, i), _) && (int)i == 10; + } + + static bool Test3(C2 u) + { + return u is var (_, ((_, i), _, _)) && (int)i == 10; + } +} + +public class C : System.Runtime.CompilerServices.ITuple +{ + int System.Runtime.CompilerServices.ITuple.Length => 2; + object System.Runtime.CompilerServices.ITuple.this[int i] => i * 10; +} + +class C2 : System.Runtime.CompilerServices.ITuple +{ + private readonly S1? _value; + public C2(S1? x) { _value = x; } + int System.Runtime.CompilerServices.ITuple.Length => 2; + object System.Runtime.CompilerServices.ITuple.this[int i] => _value; +} + +static class Extensions +{ + public static void Deconstruct(this object o, out S1? x, out int y, out int z) + { + x = (S1?)o; + y = 2; + z = 3; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "FalseFalseTrueFalse FalseFalseTrueFalse FalseFalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_04_Var_ITuple_03() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(new S1(new C()))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2((new S1(10), -1))); + System.Console.Write(Test2((new S1(new C()), -1))); + System.Console.Write(Test2((null, -1))); + + System.Console.Write(' '); + System.Console.Write(Test3(new C2(new S1(10)))); + System.Console.Write(Test3(new C2(new S1(new C())))); + System.Console.Write(Test3(new C2(null))); + } + + static bool Test1(S1 u) + { + return u is var (_, i) && (int)i == 10; + } + + static bool Test2((S1, int) u) + { + return u is var ((_, i), _) && (int)i == 10; + } + + static bool Test3(C2 u) + { + return u is var (_, ((_, i), _, _)) && (int)i == 10; + } +} + +public class C : System.Runtime.CompilerServices.ITuple +{ + int System.Runtime.CompilerServices.ITuple.Length => 2; + object System.Runtime.CompilerServices.ITuple.this[int i] => i * 10; +} + +class C2 : System.Runtime.CompilerServices.ITuple +{ + private readonly S1 _value; + public C2(S1 x) { _value = x; } + int System.Runtime.CompilerServices.ITuple.Length => 2; + object System.Runtime.CompilerServices.ITuple.this[int i] => _value; +} + +static class Extensions +{ + public static void Deconstruct(this object o, out S1 x, out int y, out int z) + { + x = (S1)o; + y = 2; + z = 3; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "FalseTrueFalse FalseTrueFalse FalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_05_Constant_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(default)); + System.Console.Write(Test3(new S1(""11""))); + + System.Console.Write(' '); + System.Console.Write(Test4(new S1(11))); + System.Console.Write(Test4(default)); + System.Console.Write(Test4(new S1(""11""))); + + System.Console.Write(' '); + System.Console.Write(Test5(new S1(10))); + System.Console.Write(Test5(default(S1))); + System.Console.Write(Test5(new S1(""11""))); + System.Console.Write(Test5(new S1(0))); + System.Console.Write(Test5(null)); + } + + static bool Test1(S1 u) + { + return u is 10; + } + + static bool Test2(S1 u) + { + return u is 10 or 11; + } + + static bool Test3(S1 u) + { + return u is ""11"" and ['1', '1']; + } + + static bool Test4(S1 u) + { + return u is null; + } + + static bool Test5(S1? u) + { + return u is 10; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseTrue FalseFalseTrue FalseTrueFalse TrueFalseFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 29 (0x1d) + .maxstack 2 + .locals init (object V_0) + IL_0000: ldarga.s V_0 + IL_0002: call ""object S1.Value.get"" + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: isinst ""int"" + IL_000e: brfalse.s IL_001b + IL_0010: ldloc.0 + IL_0011: unbox.any ""int"" + IL_0016: ldc.i4.s 10 + IL_0018: ceq + IL_001a: ret + IL_001b: ldc.i4.0 + IL_001c: ret +} +"); + verifier.VerifyIL("Program.Test5", @" +{ + // Code size 46 (0x2e) + .maxstack 2 + .locals init (S1 V_0, + object V_1) + IL_0000: ldarga.s V_0 + IL_0002: call ""readonly bool S1?.HasValue.get"" + IL_0007: brfalse.s IL_002c + IL_0009: ldarga.s V_0 + IL_000b: call ""readonly S1 S1?.GetValueOrDefault()"" + IL_0010: stloc.0 + IL_0011: ldloca.s V_0 + IL_0013: call ""object S1.Value.get"" + IL_0018: stloc.1 + IL_0019: ldloc.1 + IL_001a: isinst ""int"" + IL_001f: brfalse.s IL_002c + IL_0021: ldloc.1 + IL_0022: unbox.any ""int"" + IL_0027: ldc.i4.s 10 + IL_0029: ceq + IL_002b: ret + IL_002c: ldc.i4.0 + IL_002d: ret +} +"); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseTrue FalseFalseTrue FalseTrueFalse TrueFalseFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (47,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is 10; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(47, 21), + // (52,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is 10 or 11; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(52, 21), + // (52,27): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is 10 or 11; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "11").WithArguments("unions").WithLocation(52, 27), + // (57,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is "11" and ['1', '1']; + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"""11""").WithArguments("unions").WithLocation(57, 21), + // (62,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "null").WithArguments("unions").WithLocation(62, 21), + // (67,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is 10; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(67, 21) + ); + } + + [Fact] + public void UnionMatching_05_Constant_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(default)); + System.Console.Write(Test3(new S1(""11""))); + + System.Console.Write(' '); + System.Console.Write(Test4(new S1(11))); + System.Console.Write(Test4(default)); + System.Console.Write(Test4(new S1(""11""))); + } + + static bool Test1(S1 u) + { + return u is 10; + } + + static bool Test2(S1 u) + { + return u is 10 or 11; + } + + static bool Test3(S1 u) + { + return u is ""11"" and ['1', '1']; + } + + static bool Test4(S1 u) + { + return u is null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseTrue FalseFalseTrue FalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 31 (0x1f) + .maxstack 2 + .locals init (object V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_001d + IL_0003: ldarg.0 + IL_0004: callvirt ""object S1.Value.get"" + IL_0009: stloc.0 + IL_000a: ldloc.0 + IL_000b: isinst ""int"" + IL_0010: brfalse.s IL_001d + IL_0012: ldloc.0 + IL_0013: unbox.any ""int"" + IL_0018: ldc.i4.s 10 + IL_001a: ceq + IL_001c: ret + IL_001d: ldc.i4.0 + IL_001e: ret +} +"); + } + + [Fact] + public void UnionMatching_06_Constant_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object _value; + public C1() {} + public C1(int x) { _value = x; } + public C1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(11))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new C1(11))); + System.Console.Write(Test2(new C1())); + System.Console.Write(Test2(new C1(""11""))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1? u) + { + return u is null; + } + + static bool Test2(C1 u) + { + return u is null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "FalseTrueFalseTrue FalseTrueFalseTrue").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 19 (0x13) + .maxstack 1 + .locals init (bool V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""object C1.Value.get"" + IL_0009: brtrue.s IL_000f + IL_000b: ldc.i4.1 + IL_000c: stloc.0 + IL_000d: br.s IL_0011 + IL_000f: ldc.i4.0 + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: ret +} +"); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 34 (0x22) + .maxstack 1 + .locals init (S1 V_0, + bool V_1) + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1?.HasValue.get"" + IL_0007: brfalse.s IL_001a + IL_0009: ldarga.s V_0 + IL_000b: call ""S1 S1?.GetValueOrDefault()"" + IL_0010: stloc.0 + IL_0011: ldloca.s V_0 + IL_0013: call ""object S1.Value.get"" + IL_0018: brtrue.s IL_001e + IL_001a: ldc.i4.1 + IL_001b: stloc.1 + IL_001c: br.s IL_0020 + IL_001e: ldc.i4.0 + IL_001f: stloc.1 + IL_0020: ldloc.1 + IL_0021: ret +} +"); + } + + [Fact] + public void UnionMatching_06_Constant_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object _value; + public C1() {} + public C1(int x) { _value = x; } + public C1(C2 x) { _value = x; } + public object Value => _value; +} + +class C2; + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + private readonly object _value; + public S2(int x) { _value = x; } + public S2(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static bool Test1(S1? u) + { +#line 100 + return u is (string)null; + } + + static bool Test2(C1 u) + { +#line 200 + return u is (string)null; + } + + static bool Test3(S1 u) + { +#line 300 + return u is (string)null; + } + + static bool Test4(C2 u) + { +#line 400 + return u is (string)null; + } + + static bool Test5(S2 u) + { +#line 500 + return u is null; + } + + static bool Test6(C2 u) + { +#line 600 + return u is (object)null; + } + + static bool Test7(string u) + { +#line 700 + return u is (object)null; + } + + static bool Test8(S2 u) + { +#line 800 + return u is (object)null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,21): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // return u is (string)null; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "(string)null").WithArguments("S1").WithLocation(100, 21), + // (100,21): error CS0029: Cannot implicitly convert type 'string' to 'int' + // return u is (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "int").WithLocation(100, 21), + // (100,21): error CS0029: Cannot implicitly convert type 'string' to 'C2' + // return u is (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "C2").WithLocation(100, 21), + // (200,21): error CS9372: An expression of type 'C1' cannot be handled by this pattern, see additional errors at this location. + // return u is (string)null; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "(string)null").WithArguments("C1").WithLocation(200, 21), + // (200,21): error CS0029: Cannot implicitly convert type 'string' to 'int' + // return u is (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "int").WithLocation(200, 21), + // (200,21): error CS0029: Cannot implicitly convert type 'string' to 'C2' + // return u is (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "C2").WithLocation(200, 21), + // (300,21): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // return u is (string)null; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "(string)null").WithArguments("S1").WithLocation(300, 21), + // (300,21): error CS0029: Cannot implicitly convert type 'string' to 'int' + // return u is (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "int").WithLocation(300, 21), + // (300,21): error CS0029: Cannot implicitly convert type 'string' to 'C2' + // return u is (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "C2").WithLocation(300, 21), + // (400,21): error CS0029: Cannot implicitly convert type 'string' to 'C2' + // return u is (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "C2").WithLocation(400, 21), + // (600,21): error CS0266: Cannot implicitly convert type 'object' to 'C2'. An explicit conversion exists (are you missing a cast?) + // return u is (object)null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "(object)null").WithArguments("object", "C2").WithLocation(600, 21), + // (700,21): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // return u is (object)null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "(object)null").WithArguments("object", "string").WithLocation(700, 21), + // (800,21): error CS9372: An expression of type 'S2' cannot be handled by this pattern, see additional errors at this location. + // return u is (object)null; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "(object)null").WithArguments("S2").WithLocation(800, 21), + // (800,21): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // return u is (object)null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "(object)null").WithArguments("object", "int").WithLocation(800, 21), + // (800,21): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // return u is (object)null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "(object)null").WithArguments("object", "string").WithLocation(800, 21) + ); + } + + [Fact] + public void UnionMatching_07_Constant_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test4(new S1(11))); + System.Console.Write(Test4(default)); + System.Console.Write(Test4(new S1(""11""))); + + System.Console.Write(' '); + System.Console.Write(Test10(new S1(10))); + System.Console.Write(Test10(default)); + System.Console.Write(Test10(new S1(""11""))); + System.Console.Write(Test10(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test40(new S1(11))); + System.Console.Write(Test40(default)); + System.Console.Write(Test40(new S1(""11""))); + + System.Console.Write(' '); + System.Console.Write(Test100(new S1(10))); + System.Console.Write(Test100(default)); + System.Console.Write(Test100(new S1(""11""))); + System.Console.Write(Test100(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test400(new S1(11))); + System.Console.Write(Test400(default)); + System.Console.Write(Test400(new S1(""11""))); + } + + const int _int_10 = 10; + const string _string_null = null; + const object _object_null = null; + + static bool Test1(S1 u) + { + return u switch { _int_10 => true, _ => false }; + } + + static bool Test4(S1 u) + { + return u switch { _string_null => true, _ => false }; + } + + static bool Test10(S1 u) + { + return u is _int_10; + } + + static bool Test40(S1 u) + { + return u is _string_null; + } + + static bool Test100(S1 u) + { + switch (u) + { + case _int_10: return true; + }; + + return false; + } + + static bool Test400(S1 u) + { + switch (u) + { + case _string_null: return true; + }; + + return false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalse FalseTrueFalse TrueFalseFalseFalse FalseTrueFalse TrueFalseFalseFalse FalseTrueFalse").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_07_Constant_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + const object _object_null = null; + + static bool Test5(S1 u) + { + return u switch { _object_null => true, _ => false }; + } + + static bool Test50(S1 u) + { + return u is _object_null; + } + + static bool Test500(S1 u) + { + switch (u) + { + case _object_null: return true; + }; + + return false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (17,27): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // return u switch { _object_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "_object_null").WithArguments("S1").WithLocation(17, 27), + // (17,27): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // return u switch { _object_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "int").WithLocation(17, 27), + // (17,27): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // return u switch { _object_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "string").WithLocation(17, 27), + // (22,21): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // return u is _object_null; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "_object_null").WithArguments("S1").WithLocation(22, 21), + // (22,21): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // return u is _object_null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "int").WithLocation(22, 21), + // (22,21): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // return u is _object_null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "string").WithLocation(22, 21), + // (29,18): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // case _object_null: return true; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "_object_null").WithArguments("S1").WithLocation(29, 18), + // (29,18): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // case _object_null: return true; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "int").WithLocation(29, 18), + // (29,18): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // case _object_null: return true; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "string").WithLocation(29, 18) + ); + } + + [Fact] + public void UnionMatching_07_Constant_03() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test10(new S1(10))); + System.Console.Write(Test10(default(S1))); + System.Console.Write(Test10(new S1(""11""))); + System.Console.Write(Test10(new S1(0))); + System.Console.Write(Test10(null)); + + System.Console.Write(' '); + System.Console.Write(Test100(new S1(10))); + System.Console.Write(Test100(default(S1))); + System.Console.Write(Test100(new S1(""11""))); + System.Console.Write(Test100(new S1(0))); + System.Console.Write(Test100(null)); + } + + const int _int_10 = 10; + + static bool Test1(S1? u) + { + return u switch { _int_10 => true, _ => false }; + } + + static bool Test10(S1? u) + { + return u is _int_10; + } + + static bool Test100(S1? u) + { + switch (u) + { + case _int_10: return true; + }; + + return false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseFalse TrueFalseFalseFalseFalse TrueFalseFalseFalseFalse").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_07_Constant_04() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + const string _string_null = null; + const object _object_null = null; + + static bool Test4(S1? u) + { + return u switch { _string_null => true, _ => false }; + } + + static bool Test5(S1? u) + { + return u switch { _object_null => true, _ => false }; + } + + static bool Test40(S1? u) + { + return u is _string_null; + } + + static bool Test50(S1? u) + { + return u is _object_null; + } + + static bool Test400(S1? u) + { + switch (u) + { + case _string_null: return true; + }; + + return false; + } + + static bool Test500(S1? u) + { + switch (u) + { + case _object_null: return true; + }; + + return false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (18,27): error CS9135: A constant value of type 'S1' is expected + // return u switch { _string_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "_string_null").WithArguments("S1").WithLocation(18, 27), + // (23,27): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // return u switch { _object_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "_object_null").WithArguments("S1").WithLocation(23, 27), + // (23,27): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // return u switch { _object_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "int").WithLocation(23, 27), + // (23,27): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // return u switch { _object_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "string").WithLocation(23, 27), + // (28,21): error CS9135: A constant value of type 'S1' is expected + // return u is _string_null; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "_string_null").WithArguments("S1").WithLocation(28, 21), + // (33,21): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // return u is _object_null; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "_object_null").WithArguments("S1").WithLocation(33, 21), + // (33,21): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // return u is _object_null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "int").WithLocation(33, 21), + // (33,21): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // return u is _object_null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "string").WithLocation(33, 21), + // (40,18): error CS9135: A constant value of type 'S1' is expected + // case _string_null: return true; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "_string_null").WithArguments("S1").WithLocation(40, 18), + // (50,18): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // case _object_null: return true; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "_object_null").WithArguments("S1").WithLocation(50, 18), + // (50,18): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // case _object_null: return true; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "int").WithLocation(50, 18), + // (50,18): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // case _object_null: return true; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "string").WithLocation(50, 18) + ); + } + + [Fact] + public void UnionMatching_07_Constant_05() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(S1 x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(null)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test4(new S1(11))); + System.Console.Write(Test4(null)); + System.Console.Write(Test4(new S1(""11""))); + System.Console.Write(Test4(new S1((string)null))); + + System.Console.Write(' '); + System.Console.Write(Test10(new S1(10))); + System.Console.Write(Test10(null)); + System.Console.Write(Test10(new S1(""11""))); + System.Console.Write(Test10(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test40(new S1(11))); + System.Console.Write(Test40(null)); + System.Console.Write(Test40(new S1(""11""))); + System.Console.Write(Test40(new S1((string)null))); + + System.Console.Write(' '); + System.Console.Write(Test100(new S1(10))); + System.Console.Write(Test100(null)); + System.Console.Write(Test100(new S1(""11""))); + System.Console.Write(Test100(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test400(new S1(11))); + System.Console.Write(Test400(null)); + System.Console.Write(Test400(new S1(""11""))); + System.Console.Write(Test400(new S1((string)null))); + } + + const int _int_10 = 10; + const S1 _S1_null = null; + + static bool Test1(S1 u) + { + return u switch { _int_10 => true, _ => false }; + } + + static bool Test4(S1 u) + { + return u switch { _S1_null => true, _ => false }; + } + + static bool Test10(S1 u) + { + return u is _int_10; + } + + static bool Test40(S1 u) + { + return u is _S1_null; + } + + static bool Test100(S1 u) + { + switch (u) + { + case _int_10: return true; + }; + + return false; + } + + static bool Test400(S1 u) + { + switch (u) + { + case _S1_null: return true; + }; + + return false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalse FalseTrueFalseTrue TrueFalseFalseFalse FalseTrueFalseTrue TrueFalseFalseFalse FalseTrueFalseTrue").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test4", +@" +{ + // Code size 19 (0x13) + .maxstack 1 + .locals init (bool V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""object S1.Value.get"" + IL_0009: brtrue.s IL_000f + IL_000b: ldc.i4.1 + IL_000c: stloc.0 + IL_000d: br.s IL_0011 + IL_000f: ldc.i4.0 + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: ret +} +"); + + verifier.VerifyIL("Program.Test40", +@" +{ + // Code size 19 (0x13) + .maxstack 1 + .locals init (bool V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""object S1.Value.get"" + IL_0009: brtrue.s IL_000f + IL_000b: ldc.i4.1 + IL_000c: stloc.0 + IL_000d: br.s IL_0011 + IL_000f: ldc.i4.0 + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: ret +} +"); + + verifier.VerifyIL("Program.Test400", +@" +{ + // Code size 15 (0xf) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""object S1.Value.get"" + IL_0009: brtrue.s IL_000d + IL_000b: ldc.i4.1 + IL_000c: ret + IL_000d: ldc.i4.0 + IL_000e: ret +} +"); + } + + [Fact] + public void UnionMatching_07_Constant_06() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + const string _string_null = null; + const object _object_null = null; + + static bool Test4(S1 u) + { + return u switch { _string_null => true, _ => false }; + } + + static bool Test5(S1 u) + { + return u switch { _object_null => true, _ => false }; + } + + static bool Test40(S1 u) + { + return u is _string_null; + } + + static bool Test50(S1 u) + { + return u is _object_null; + } + + static bool Test400(S1 u) + { + switch (u) + { + case _string_null: return true; + }; + + return false; + } + + static bool Test500(S1 u) + { + switch (u) + { + case _object_null: return true; + }; + + return false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (18,27): error CS9135: A constant value of type 'S1' is expected + // return u switch { _string_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "_string_null").WithArguments("S1").WithLocation(18, 27), + // (23,27): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // return u switch { _object_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "_object_null").WithArguments("S1").WithLocation(23, 27), + // (23,27): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // return u switch { _object_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "int").WithLocation(23, 27), + // (23,27): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // return u switch { _object_null => true, _ => false }; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "string").WithLocation(23, 27), + // (28,21): error CS9135: A constant value of type 'S1' is expected + // return u is _string_null; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "_string_null").WithArguments("S1").WithLocation(28, 21), + // (33,21): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // return u is _object_null; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "_object_null").WithArguments("S1").WithLocation(33, 21), + // (33,21): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // return u is _object_null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "int").WithLocation(33, 21), + // (33,21): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // return u is _object_null; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "string").WithLocation(33, 21), + // (40,18): error CS9135: A constant value of type 'S1' is expected + // case _string_null: return true; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "_string_null").WithArguments("S1").WithLocation(40, 18), + // (50,18): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // case _object_null: return true; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "_object_null").WithArguments("S1").WithLocation(50, 18), + // (50,18): error CS0266: Cannot implicitly convert type 'object' to 'int'. An explicit conversion exists (are you missing a cast?) + // case _object_null: return true; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "int").WithLocation(50, 18), + // (50,18): error CS0266: Cannot implicitly convert type 'object' to 'string'. An explicit conversion exists (are you missing a cast?) + // case _object_null: return true; + Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "_object_null").WithArguments("object", "string").WithLocation(50, 18) + ); + } + + [Fact] + public void UnionMatching_08_Recursive_Property_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public object Value => _value; +} + +struct S2 +{ + public T Value; +} + +class A; +class B; + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test1(new S1(new S2() { Value = 0 }))); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test2(new S1(new S2() { Value = 0 }))); + System.Console.Write(Test2(new S1(new S2() { Value = 11 }))); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(new S2() { Value = 11 }))); + System.Console.Write(Test3(default)); + System.Console.Write(Test3(new S1(new S2() { Value = ""11"" }))); + } + + static bool Test1(S1 u) + { + return u is S2 { Value: 10 }; + } + + static bool Test2(S1 u) + { + return u is S2 { Value: 10 or 11 }; + } + + static bool Test3(S1 u) + { + return u is S2 { Value: ""11"" } and { Value: ['1', '1'] }; + } + + static bool Test4(S1 u) + { +#line 58 + return u is S2 { Value: not A or B }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseTrue FalseFalseTrue" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics( + // (58,50): warning CS9336: The pattern is redundant. + // return u is S2 { Value: not A or B }; + Diagnostic(ErrorCode.WRN_RedundantPattern, "B").WithLocation(58, 50) + ); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseTrue FalseFalseTrue" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics( + // (58,50): warning CS9336: The pattern is redundant. + // return u is S2 { Value: not A or B }; + Diagnostic(ErrorCode.WRN_RedundantPattern, "B").WithLocation(58, 50) + ); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (44,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 { Value: 10 }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 { Value: 10 }").WithArguments("unions").WithLocation(44, 21), + // (49,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 { Value: 10 or 11 }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 { Value: 10 or 11 }").WithArguments("unions").WithLocation(49, 21), + // (54,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 { Value: "11" } and { Value: ['1', '1'] }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"S2 { Value: ""11"" }").WithArguments("unions").WithLocation(54, 21), + // (58,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 { Value: not A or B }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 { Value: not A or B }").WithArguments("unions").WithLocation(58, 21), + // (58,50): warning CS9336: The pattern is redundant. + // return u is S2 { Value: not A or B }; + Diagnostic(ErrorCode.WRN_RedundantPattern, "B").WithLocation(58, 50) + ); + } + + [Fact] + public void UnionMatching_08_Recursive_Property_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public object Value => _value; +} + +struct S2 +{ + public T Value; +} + +class A; +class B; + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test1(new S1(new S2() { Value = 0 }))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test2(new S1(new S2() { Value = 0 }))); + System.Console.Write(Test2(new S1(new S2() { Value = 11 }))); + System.Console.Write(Test2(null)); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(new S2() { Value = 11 }))); + System.Console.Write(Test3(default(S1))); + System.Console.Write(Test3(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test3(null)); + } + + static bool Test1(S1? u) + { + return u is S2 { Value: 10 }; + } + + static bool Test2(S1? u) + { + return u is S2 { Value: 10 or 11 }; + } + + static bool Test3(S1? u) + { + return u is S2 { Value: ""11"" } and { Value: ['1', '1'] }; + } + + static bool Test4(S1? u) + { +#line 58 + return u is S2 { Value: not A or B }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalseFalse TrueFalseFalseFalseTrueFalse FalseFalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics( + // (58,50): warning CS9336: The pattern is redundant. + // return u is S2 { Value: not A or B }; + Diagnostic(ErrorCode.WRN_RedundantPattern, "B").WithLocation(58, 50) + ); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalseFalse TrueFalseFalseFalseTrueFalse FalseFalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics( + // (58,50): warning CS9336: The pattern is redundant. + // return u is S2 { Value: not A or B }; + Diagnostic(ErrorCode.WRN_RedundantPattern, "B").WithLocation(58, 50) + ); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (47,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 { Value: 10 }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 { Value: 10 }").WithArguments("unions").WithLocation(47, 21), + // (52,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 { Value: 10 or 11 }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 { Value: 10 or 11 }").WithArguments("unions").WithLocation(52, 21), + // (57,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 { Value: "11" } and { Value: ['1', '1'] }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"S2 { Value: ""11"" }").WithArguments("unions").WithLocation(57, 21), + // (58,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 { Value: not A or B }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 { Value: not A or B }").WithArguments("unions").WithLocation(58, 21), + // (58,50): warning CS9336: The pattern is redundant. + // return u is S2 { Value: not A or B }; + Diagnostic(ErrorCode.WRN_RedundantPattern, "B").WithLocation(58, 50) + ); + } + + [Fact] + public void UnionMatching_08_Recursive_Property_03() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public object Value => _value; +} + +struct S2 +{ + public T Value; +} + +class A; +class B; + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test1(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test1(new S1(new S2() { Value = 0 }))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test2(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test2(new S1(new S2() { Value = 0 }))); + System.Console.Write(Test2(new S1(new S2() { Value = 11 }))); + System.Console.Write(Test2(null)); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(new S2() { Value = 11 }))); + System.Console.Write(Test3(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test3(null)); + } + + static bool Test1(S1 u) + { + return u is S2 { Value: 10 }; + } + + static bool Test2(S1 u) + { + return u is S2 { Value: 10 or 11 }; + } + + static bool Test3(S1 u) + { + return u is S2 { Value: ""11"" } and { Value: ['1', '1'] }; + } + + static bool Test4(S1 u) + { +#line 58 + return u is S2 { Value: not A or B }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseTrueFalse FalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics( + // (58,50): warning CS9336: The pattern is redundant. + // return u is S2 { Value: not A or B }; + Diagnostic(ErrorCode.WRN_RedundantPattern, "B").WithLocation(58, 50) + ); + } + + [Fact] + public void UnionMatching_09_Recursive_ITuple() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(new C()))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(new C()))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1 u) + { + return u is (_, 10); + } + + static bool Test2(S1? u) + { + return u is (_, 10); + } +} + +public class C : System.Runtime.CompilerServices.ITuple +{ + int System.Runtime.CompilerServices.ITuple.Length => 2; + object System.Runtime.CompilerServices.ITuple.this[int i] => i * 10; +} + +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "FalseFalseTrue FalseFalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "FalseFalseTrue FalseFalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (27,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (_, 10); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "(_, 10)").WithArguments("unions").WithLocation(27, 21), + // (32,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (_, 10); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "(_, 10)").WithArguments("unions").WithLocation(32, 21) + ); + } + + [Fact] + public void UnionMatching_10_Recursive_Deconstruct_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public object Value => _value; +} + +struct S2 +{ + public T Value; + + public void Deconstruct(out T value, out int x) + { + value = Value; + x = 0; + } +} + +class A; +class B; + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test1(new S1(new S2() { Value = 0 }))); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test2(new S1(new S2() { Value = 0 }))); + System.Console.Write(Test2(new S1(new S2() { Value = 11 }))); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(new S2() { Value = 11 }))); + System.Console.Write(Test3(default)); + System.Console.Write(Test3(new S1(new S2() { Value = ""11"" }))); + } + + static bool Test1(S1 u) + { + return u is S2 (10, _); + } + + static bool Test2(S1 u) + { + return u is S2 (10 or 11, _); + } + + static bool Test3(S1 u) + { + return u is S2 (""11"", _) and (['1', '1'], _); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseTrue FalseFalseTrue" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseTrue FalseFalseTrue" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (50,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 (10, _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 (10, _)").WithArguments("unions").WithLocation(50, 21), + // (55,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 (10 or 11, _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 (10 or 11, _)").WithArguments("unions").WithLocation(55, 21), + // (60,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 ("11", _) and (['1', '1'], _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"S2 (""11"", _)").WithArguments("unions").WithLocation(60, 21) + ); + } + + [Fact] + public void UnionMatching_10_Recursive_Deconstruct_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public object Value => _value; +} + +struct S2 +{ + public T Value; + + public void Deconstruct(out T value, out int x) + { + value = Value; + x = 0; + } +} + +class A; +class B; + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test1(new S1(new S2() { Value = 0 }))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(new S2() { Value = 10 }))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test2(new S1(new S2() { Value = 0 }))); + System.Console.Write(Test2(new S1(new S2() { Value = 11 }))); + System.Console.Write(Test2(null)); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(new S2() { Value = 11 }))); + System.Console.Write(Test3(default(S1))); + System.Console.Write(Test3(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(Test3(null)); + } + + static bool Test1(S1? u) + { + return u is S2 (10, _); + } + + static bool Test2(S1? u) + { + return u is S2 (10 or 11, _); + } + + static bool Test3(S1? u) + { + return u is S2 (""11"", _) and (['1', '1'], _); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalseFalse TrueFalseFalseFalseTrueFalse FalseFalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalseFalse TrueFalseFalseFalseTrueFalse FalseFalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (53,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 (10, _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 (10, _)").WithArguments("unions").WithLocation(53, 21), + // (58,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 (10 or 11, _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "S2 (10 or 11, _)").WithArguments("unions").WithLocation(58, 21), + // (63,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is S2 ("11", _) and (['1', '1'], _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"S2 (""11"", _)").WithArguments("unions").WithLocation(63, 21) + ); + } + + [Fact] + public void UnionMatching_11_Type() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1 u) + { + return u is int; + } + + static bool Test2(S1? u) + { + return u is int; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseFalseTrue TrueFalseFalseTrueFalse").VerifyDiagnostics(); + + var tree = comp.SyntaxTrees[0]; + + AssertEx.Equal(["u is int", "u is int"], tree.GetRoot().DescendantNodes().OfType().Select(b => b.ToString())); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 16 (0x10) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: call ""object S1.Value.get"" + IL_0007: isinst ""int"" + IL_000c: ldnull + IL_000d: cgt.un + IL_000f: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 35 (0x23) + .maxstack 2 + .locals init (S1 V_0) + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1?.HasValue.get"" + IL_0007: brfalse.s IL_0021 + IL_0009: ldarga.s V_0 + IL_000b: call ""S1 S1?.GetValueOrDefault()"" + IL_0010: stloc.0 + IL_0011: ldloca.s V_0 + IL_0013: call ""object S1.Value.get"" + IL_0018: isinst ""int"" + IL_001d: ldnull + IL_001e: cgt.un + IL_0020: ret + IL_0021: ldc.i4.0 + IL_0022: ret +} +"); + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseTrue TrueFalseFalseTrueFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (29,16): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is int; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "u is int").WithArguments("unions").WithLocation(29, 16), + // (34,16): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is int; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "u is int").WithArguments("unions").WithLocation(34, 16) + ); + } + + [Fact] + public void UnionMatching_12_Type_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(default)); + System.Console.Write(Test3(new S1(""11""))); + } + + static bool Test1(S1 u) + { + return u switch { int => true, _ => false }; + } + + static bool Test3(S1 u) + { + return u is string and ['1', '1']; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseTrue FalseFalseTrue" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseTrue FalseFalseTrue" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (28,27): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u switch { int => true, _ => false }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int").WithArguments("unions").WithLocation(28, 27), + // (33,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is string and ['1', '1']; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "string").WithArguments("unions").WithLocation(33, 21) + ); + } + + [Fact] + public void UnionMatching_12_Type_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(default(S1))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(null)); + } + + static bool Test1(S1? u) + { + return u switch { int => true, _ => false }; + } + + static bool Test3(S1? u) + { + return u is string and ['1', '1']; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseTrueFalse FalseFalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseTrueFalse FalseFalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (30,27): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u switch { int => true, _ => false }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int").WithArguments("unions").WithLocation(30, 27), + // (35,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is string and ['1', '1']; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "string").WithArguments("unions").WithLocation(35, 21) + ); + } + + [Fact] + public void UnionMatching_13_Declaration_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + } + + static bool Test1(S1 u) + { + return u is int x; + } + + static bool Test2(S1 u) + { + return u is int x ? (x == 10 || x == 11) : false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseTrue TrueFalseFalseFalseTrue").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseTrue TrueFalseFalseFalseTrue").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (30,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is int x; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int x").WithArguments("unions").WithLocation(30, 21), + // (35,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is int x ? (x == 10 || x == 11) : false; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int x").WithArguments("unions").WithLocation(35, 21) + ); + } + + [Fact] + public void UnionMatching_13_Declaration_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1? u) + { + return u is int x; + } + + static bool Test2(S1? u) + { + return u is int x ? (x == 10 || x == 11) : false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseTrueFalse TrueFalseFalseFalseTrueFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseTrueFalse TrueFalseFalseFalseTrueFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (32,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is int x; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int x").WithArguments("unions").WithLocation(32, 21), + // (37,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is int x ? (x == 10 || x == 11) : false; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int x").WithArguments("unions").WithLocation(37, 21) + ); + } + + [Fact] + public void UnionMatching_14_Negated_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(default)); + System.Console.Write(Test3(new S1(""11""))); + + System.Console.Write(' '); + System.Console.Write(Test4(new S1(11))); + System.Console.Write(Test4(default)); + System.Console.Write(Test4(new S1(""11""))); + + System.Console.Write(' '); + System.Console.Write(Test5(new S1(10))); + System.Console.Write(Test5(default)); + System.Console.Write(Test5(new S1(""11""))); + System.Console.Write(Test5(new S1(0))); + } + + static bool Test1(S1 u) + { + return u is not 10; + } + + static bool Test2(S1 u) + { + return u is not (10 or 11); + } + + static bool Test3(S1 u) + { + return u is not (""11"" and ['1', '1']); + } + + static bool Test4(S1 u) + { + return u is not null; + } + + static bool Test5(S1 u) + { + return u is not ({ } and int); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "FalseTrueTrueTrue FalseTrueTrueTrueFalse TrueTrueFalse TrueFalseTrue FalseTrueTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "FalseTrueTrueTrue FalseTrueTrueTrueFalse TrueTrueFalse TrueFalseTrue FalseTrueTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (46,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not 10; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "not 10").WithArguments("unions").WithLocation(46, 21), + // (51,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not (10 or 11); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "not (10 or 11)").WithArguments("unions").WithLocation(51, 21), + // (56,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not ("11" and ['1', '1']); + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"not (""11"" and ['1', '1'])").WithArguments("unions").WithLocation(56, 21), + // (61,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "not null").WithArguments("unions").WithLocation(61, 21), + // (66,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not ({ } and int); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "not ({ } and int)").WithArguments("unions").WithLocation(66, 21) + ); + } + + [Fact] + public void UnionMatching_14_Negated_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + System.Console.Write(Test2(null)); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(default(S1))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(null)); + + System.Console.Write(' '); + System.Console.Write(Test4(new S1(11))); + System.Console.Write(Test4(default(S1))); + System.Console.Write(Test4(new S1(""11""))); + System.Console.Write(Test4(null)); + + System.Console.Write(' '); + System.Console.Write(Test5(new S1(10))); + System.Console.Write(Test5(default(S1))); + System.Console.Write(Test5(new S1(""11""))); + System.Console.Write(Test5(new S1(0))); + System.Console.Write(Test5(null)); + } + + static bool Test1(S1? u) + { + return u is not 10; + } + + static bool Test2(S1? u) + { + return u is not (10 or 11); + } + + static bool Test3(S1? u) + { + return u is not (""11"" and ['1', '1']); + } + + static bool Test4(S1? u) + { + return u is not null; + } + + static bool Test5(S1? u) + { + return u is not ({ } and int); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "FalseTrueTrueTrueFalse FalseTrueTrueTrueFalseFalse TrueTrueFalseFalse TrueFalseTrueFalse FalseTrueTrueFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "FalseTrueTrueTrueFalse FalseTrueTrueTrueFalseFalse TrueTrueFalseFalse TrueFalseTrueFalse FalseTrueTrueFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (51,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not 10; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "not 10").WithArguments("unions").WithLocation(51, 21), + // (56,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not (10 or 11); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "not (10 or 11)").WithArguments("unions").WithLocation(56, 21), + // (61,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not ("11" and ['1', '1']); + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"not (""11"" and ['1', '1'])").WithArguments("unions").WithLocation(61, 21), + // (66,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "not null").WithArguments("unions").WithLocation(66, 21), + // (71,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not ({ } and int); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "not ({ } and int)").WithArguments("unions").WithLocation(71, 21) + ); + } + + [Fact] + public void UnionMatching_15_Negated() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test5(new S1(11))); + System.Console.Write(Test5(default)); + System.Console.Write(Test5(new S1(""11""))); + + System.Console.Write(' '); + System.Console.Write(Test6(new S1(11))); + System.Console.Write(Test6(default)); + System.Console.Write(Test6(new S1(""11""))); + } + + static int Test5(S1 u) + { + if (u is not int x) + { + return -1; + } + + return x; + } + + static int Test6(S1 u) + { + if (u is not not not int x) + { + return -1; + } + + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "11-1-1 11-1-1").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_16_Negated() + { + var src = @" +[System.Runtime.CompilerServices.Union] +sealed class C1 +{ + private readonly object _value; + public C1(int x) { _value = x; } + public C1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static int Test5(C1 u) + { +#line 14 + if (u is not int x) + { + return -1; + } +#line 19 + return x; + } + + static int Test6(S1 u) + { + if (u is not not int y) + { + return y - 1; + } +#line 29 + return y; + } + + static int Test7(S1? u) + { +#line 34 + if (u is not int z) + { + return -1; + } +#line 39 + return z; + } + + static bool Test8(S1 u) + { +#line 44 + return u is not (S1 and int); + } + + static bool Test9(S1? u) + { +#line 49 + return u is not (S1 and int); + } +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + // There is an implicit null check for class union types and for Nullable. + comp.VerifyDiagnostics( + // (14,26): error CS8780: A variable may not be declared within a 'not' or 'or' pattern. + // if (u is not int x) + Diagnostic(ErrorCode.ERR_DesignatorBeneathPatternCombinator, "x").WithLocation(14, 26), + // (19,16): error CS0165: Use of unassigned local variable 'x' + // return x; + Diagnostic(ErrorCode.ERR_UseDefViolation, "x").WithArguments("x").WithLocation(19, 16), + // (29,16): error CS0165: Use of unassigned local variable 'y' + // return y; + Diagnostic(ErrorCode.ERR_UseDefViolation, "y").WithArguments("y").WithLocation(29, 16), + // (34,26): error CS8780: A variable may not be declared within a 'not' or 'or' pattern. + // if (u is not int z) + Diagnostic(ErrorCode.ERR_DesignatorBeneathPatternCombinator, "z").WithLocation(34, 26), + // (39,16): error CS0165: Use of unassigned local variable 'z' + // return z; + Diagnostic(ErrorCode.ERR_UseDefViolation, "z").WithArguments("z").WithLocation(39, 16), + + // https://github.com/dotnet/roslyn/issues/82636: The following diagnostics is somewhat confusing in these cases. + // A type cannot be handled by the pattern of the same type. + // Syntactially it is not obvious that we are doing a union matching. + + // (44,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'S1'. + // return u is not (S1 and int); + Diagnostic(ErrorCode.ERR_PatternWrongType, "S1").WithArguments("S1", "S1").WithLocation(44, 26), + // (49,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'S1'. + // return u is not (S1 and int); + Diagnostic(ErrorCode.ERR_PatternWrongType, "S1").WithArguments("S1", "S1").WithLocation(49, 26) + ); + } + + [Fact] + public void UnionMatching_17_Negated_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object _value; + public C1() {} + public C1(int x) { _value = x; } + public C1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(11))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new C1(11))); + System.Console.Write(Test2(new C1())); + System.Console.Write(Test2(new C1(""11""))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1? u) + { + return u is not null; + } + + static bool Test2(C1 u) + { + return u is not null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseTrueFalse TrueFalseTrueFalse").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 30 (0x1e) + .maxstack 2 + .locals init (S1 V_0) + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1?.HasValue.get"" + IL_0007: brfalse.s IL_001c + IL_0009: ldarga.s V_0 + IL_000b: call ""S1 S1?.GetValueOrDefault()"" + IL_0010: stloc.0 + IL_0011: ldloca.s V_0 + IL_0013: call ""object S1.Value.get"" + IL_0018: ldnull + IL_0019: cgt.un + IL_001b: ret + IL_001c: ldc.i4.0 + IL_001d: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 15 (0xf) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000d + IL_0003: ldarg.0 + IL_0004: callvirt ""object C1.Value.get"" + IL_0009: ldnull + IL_000a: cgt.un + IL_000c: ret + IL_000d: ldc.i4.0 + IL_000e: ret +} +"); + } + + [Fact] + public void UnionMatching_17_Negated_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object _value; + public C1() {} + public C1(int x) { _value = x; } + public C1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(11))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new C1(11))); + System.Console.Write(Test2(new C1())); + System.Console.Write(Test2(new C1(""11""))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1? u) + { + return u is not (string)null; + } + + static bool Test2(C1 u) + { + return u is not (string)null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseTrueFalse TrueFalseTrueFalse").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 30 (0x1e) + .maxstack 2 + .locals init (S1 V_0) + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1?.HasValue.get"" + IL_0007: brfalse.s IL_001c + IL_0009: ldarga.s V_0 + IL_000b: call ""S1 S1?.GetValueOrDefault()"" + IL_0010: stloc.0 + IL_0011: ldloca.s V_0 + IL_0013: call ""object S1.Value.get"" + IL_0018: ldnull + IL_0019: cgt.un + IL_001b: ret + IL_001c: ldc.i4.0 + IL_001d: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 15 (0xf) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000d + IL_0003: ldarg.0 + IL_0004: callvirt ""object C1.Value.get"" + IL_0009: ldnull + IL_000a: cgt.un + IL_000c: ret + IL_000d: ldc.i4.0 + IL_000e: ret +} +"); + } + + [Fact] + public void UnionMatching_17_Negated_03() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object _value; + public C1() {} + public C1(int x) { _value = x; } + public C1(C2 x) { _value = x; } + public object Value => _value; +} + +class C2; + +class Program +{ + static bool Test1(S1? u) + { + return u is not (string)null; + } + + static bool Test2(C1 u) + { + return u is not (string)null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (27,25): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // return u is not (string)null; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "(string)null").WithArguments("S1").WithLocation(27, 25), + // (27,25): error CS0029: Cannot implicitly convert type 'string' to 'int' + // return u is not (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "int").WithLocation(27, 25), + // (27,25): error CS0029: Cannot implicitly convert type 'string' to 'C2' + // return u is not (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "C2").WithLocation(27, 25), + // (32,25): error CS9372: An expression of type 'C1' cannot be handled by this pattern, see additional errors at this location. + // return u is not (string)null; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "(string)null").WithArguments("C1").WithLocation(32, 25), + // (32,25): error CS0029: Cannot implicitly convert type 'string' to 'int' + // return u is not (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "int").WithLocation(32, 25), + // (32,25): error CS0029: Cannot implicitly convert type 'string' to 'C2' + // return u is not (string)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(string)null").WithArguments("string", "C2").WithLocation(32, 25) + ); + } + + [Fact] + public void UnionMatching_17_BinaryOr() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + System.Console.Write(Test2(new S1(""111""))); + System.Console.Write(' '); + System.Console.Write(Test3(new S1(10))); + System.Console.Write(Test3(default(S1))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(new S1(0))); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(new S1(""111""))); + System.Console.Write(Test3(null)); + } + + static bool Test2(S1 u) + { + return u is 10 or ""11""; + } + + static bool Test3(S1? u) + { + return u is 10 or ""11""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseTrueFalseFalseFalse TrueFalseTrueFalseFalseFalseFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "TrueFalseTrueFalseFalseFalse TrueFalseTrueFalseFalseFalseFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (33,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is 10 or "11"; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(33, 21), + // (33,27): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is 10 or "11"; + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"""11""").WithArguments("unions").WithLocation(33, 27), + // (38,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is 10 or "11"; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(38, 21), + // (38,27): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is 10 or "11"; + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"""11""").WithArguments("unions").WithLocation(38, 27) + ); + } + + [Fact] + public void UnionMatching_18_BinaryAnd() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(null)); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(""11""))); + } + + static string Test2(object u) + { + if (u is S1 and int x) + { + return x.ToString(); + } + + return ""_""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "10___").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_19_BinaryAnd() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(' '); + System.Console.Write(Test3(new S1(10))); + System.Console.Write(Test3(default(S1))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(null)); + } + + static string Test2(S1 u) + { + if (u is 10 and var x) + { + return x.GetType().ToString(); + } + + return ""_""; + } + + static string Test3(S1? u) + { + if (u is 10 and var x) + { + return x.GetType().ToString(); + } + + return ""_""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "System.Int32__ System.Int32___").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "System.Int32__ System.Int32___").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (27,18): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // if (u is 10 and var x) + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(27, 18), + // (37,18): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // if (u is 10 and var x) + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(37, 18) + ); + } + + [Fact] + public void UnionMatching_20_BinaryAnd() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + private readonly object _value; + public S2(int x) { _value = x; } + public S2(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1(new S2(10)))); + System.Console.Write(Test2(new S1(new S2(11)))); + System.Console.Write(Test2(null)); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(new S2()))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(new S2(""11"")))); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(new S2(10)))); + System.Console.Write(Test3(new S1(new S2(11)))); + System.Console.Write(Test3(null)); + System.Console.Write(Test3(new S1())); + System.Console.Write(Test3(new S1(new S2()))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(new S1(new S2(""11"")))); + } + + static bool Test2(object u) + { + return u is S1 and S2 and 10; + } + + static bool Test3(object u) + { + return u is S1 and (S2 and 10); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseFalseFalseFalse TrueFalseFalseFalseFalseFalseFalse").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_21_BinaryAnd() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + private readonly object _value; + public S2(S3 x) { _value = x; } + public S2(int x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S3 +{ + private readonly object _value; + public S3(int x) { _value = x; } + public S3(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1(new S2(new S3(10))))); + System.Console.Write(Test2(new S1(new S2(new S3(11))))); + System.Console.Write(Test2(null)); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(new S2()))); + System.Console.Write(Test2(new S1(new S2(new S3())))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(new S2(10)))); + System.Console.Write(Test2(new S1(new S2(11)))); + System.Console.Write(Test2(new S1(new S2(new S3(""11""))))); + + System.Console.WriteLine(); + System.Console.Write(Test3(new S1(new S2(new S3(10))))); + System.Console.Write(Test3(new S1(new S2(new S3(11))))); + System.Console.Write(Test3(null)); + System.Console.Write(Test3(new S1())); + System.Console.Write(Test3(new S1(new S2()))); + System.Console.Write(Test3(new S1(new S2(new S3())))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(new S1(new S2(10)))); + System.Console.Write(Test3(new S1(new S2(11)))); + System.Console.Write(Test3(new S1(new S2(new S3(""11""))))); + + System.Console.WriteLine(); + System.Console.Write(Test4(new S1(new S2(new S3(10))))); + System.Console.Write(Test4(new S1(new S2(new S3(11))))); + System.Console.Write(Test4(null)); + System.Console.Write(Test4(new S1())); + System.Console.Write(Test4(new S1(new S2()))); + System.Console.Write(Test4(new S1(new S2(new S3())))); + System.Console.Write(Test4(new S1(""11""))); + System.Console.Write(Test4(new S1(new S2(10)))); + System.Console.Write(Test4(new S1(new S2(11)))); + System.Console.Write(Test4(new S1(new S2(new S3(""11""))))); + + System.Console.WriteLine(); + System.Console.Write(Test5(new S1(new S2(new S3(10))))); + System.Console.Write(Test5(new S1(new S2(new S3(11))))); + System.Console.Write(Test5(null)); + System.Console.Write(Test5(new S1())); + System.Console.Write(Test5(new S1(new S2()))); + System.Console.Write(Test5(new S1(new S2(new S3())))); + System.Console.Write(Test5(new S1(""11""))); + System.Console.Write(Test5(new S1(new S2(10)))); + System.Console.Write(Test5(new S1(new S2(11)))); + System.Console.Write(Test5(new S1(new S2(new S3(""11""))))); + + System.Console.WriteLine(); + System.Console.Write(Test6(new S1(new S2(new S3(10))))); + System.Console.Write(Test6(new S1(new S2(new S3(11))))); + System.Console.Write(Test6(null)); + System.Console.Write(Test6(new S1())); + System.Console.Write(Test6(new S1(new S2()))); + System.Console.Write(Test6(new S1(new S2(new S3())))); + System.Console.Write(Test6(new S1(""11""))); + System.Console.Write(Test6(new S1(new S2(10)))); + System.Console.Write(Test6(new S1(new S2(11)))); + System.Console.Write(Test6(new S1(new S2(new S3(""11""))))); + } + + static bool Test2(object u) + { + return u is ((S1 and S2) and S3) and 10; + } + + static bool Test3(object u) + { + return u is (S1 and S2) and (S3 and 10); + } + + static bool Test4(object u) + { + return u is (S1 and (S2 and S3)) and 10; + } + + static bool Test5(object u) + { + return u is S1 and (S2 and S3 and 10); + } + + static bool Test6(object u) + { + return u is S1 and (S2 and (S3 and 10)); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: @" +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_22_BinaryAnd() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + private readonly object _value; + public S2(S3 x) { _value = x; } + public S2(int x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S3 +{ + private readonly object _value; + public S3(int x) { _value = x; } + public S3(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1(new S2(new S3(10))))); + System.Console.Write(Test2(new S1(new S2(new S3(11))))); + System.Console.Write(Test2(null)); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(new S2()))); + System.Console.Write(Test2(new S1(new S2(new S3())))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(new S2(10)))); + System.Console.Write(Test2(new S1(new S2(11)))); + System.Console.Write(Test2(new S1(new S2(new S3(""11""))))); + + System.Console.WriteLine(); + System.Console.Write(Test3(new S1(new S2(new S3(10))))); + System.Console.Write(Test3(new S1(new S2(new S3(11))))); + System.Console.Write(Test3(null)); + System.Console.Write(Test3(new S1())); + System.Console.Write(Test3(new S1(new S2()))); + System.Console.Write(Test3(new S1(new S2(new S3())))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(new S1(new S2(10)))); + System.Console.Write(Test3(new S1(new S2(11)))); + System.Console.Write(Test3(new S1(new S2(new S3(""11""))))); + + System.Console.WriteLine(); + System.Console.Write(Test4(new S1(new S2(new S3(10))))); + System.Console.Write(Test4(new S1(new S2(new S3(11))))); + System.Console.Write(Test4(null)); + System.Console.Write(Test4(new S1())); + System.Console.Write(Test4(new S1(new S2()))); + System.Console.Write(Test4(new S1(new S2(new S3())))); + System.Console.Write(Test4(new S1(""11""))); + System.Console.Write(Test4(new S1(new S2(10)))); + System.Console.Write(Test4(new S1(new S2(11)))); + System.Console.Write(Test4(new S1(new S2(new S3(""11""))))); + + System.Console.WriteLine(); + System.Console.Write(Test5(new S1(new S2(new S3(10))))); + System.Console.Write(Test5(new S1(new S2(new S3(11))))); + System.Console.Write(Test5(null)); + System.Console.Write(Test5(new S1())); + System.Console.Write(Test5(new S1(new S2()))); + System.Console.Write(Test5(new S1(new S2(new S3())))); + System.Console.Write(Test5(new S1(""11""))); + System.Console.Write(Test5(new S1(new S2(10)))); + System.Console.Write(Test5(new S1(new S2(11)))); + System.Console.Write(Test5(new S1(new S2(new S3(""11""))))); + + System.Console.WriteLine(); + System.Console.Write(Test6(new S1(new S2(new S3(10))))); + System.Console.Write(Test6(new S1(new S2(new S3(11))))); + System.Console.Write(Test6(null)); + System.Console.Write(Test6(new S1())); + System.Console.Write(Test6(new S1(new S2()))); + System.Console.Write(Test6(new S1(new S2(new S3())))); + System.Console.Write(Test6(new S1(""11""))); + System.Console.Write(Test6(new S1(new S2(10)))); + System.Console.Write(Test6(new S1(new S2(11)))); + System.Console.Write(Test6(new S1(new S2(new S3(""11""))))); + } + + static bool Test2(object u) + { + return u is ((S1 and S2) and S3) and var x and 10; + } + + static bool Test3(object u) + { + return u is (S1 and S2) and (var x and S3 and 10); + } + + static bool Test4(object u) + { + return u is (S1 and (var x and S2 and S3)) and 10; + } + + static bool Test5(object u) + { + return u is S1 and (S2 and var x and S3 and 10); + } + + static bool Test6(object u) + { + return u is S1 and (S2 and (var x and S3 and 10)); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: @" +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +TrueFalseFalseFalseFalseFalseFalseFalseFalseFalse +").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_23_Parenthesized_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(default)); + System.Console.Write(Test3(new S1(""11""))); + + System.Console.Write(' '); + System.Console.Write(Test4(new S1(11))); + System.Console.Write(Test4(default)); + System.Console.Write(Test4(new S1(""11""))); + } + + static bool Test1(S1 u) + { + return u is (10); + } + + static bool Test2(S1 u) + { + return u is (10 or 11); + } + + static bool Test3(S1 u) + { + return u is (""11"" and ['1', '1']); + } + + static bool Test4(S1 u) + { + return u is (null); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseTrue FalseFalseTrue FalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseTrue FalseFalseTrue FalseTrueFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (40,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (10); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "(10)").WithArguments("unions").WithLocation(40, 21), + // (45,22): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (10 or 11); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(45, 22), + // (45,28): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (10 or 11); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "11").WithArguments("unions").WithLocation(45, 28), + // (50,22): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is ("11" and ['1', '1']); + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"""11""").WithArguments("unions").WithLocation(50, 22), + // (55,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (null); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "(null)").WithArguments("unions").WithLocation(55, 21) + ); + } + + [Fact] + public void UnionMatching_23_Parenthesized_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + System.Console.Write(Test2(null)); + + System.Console.Write(' '); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(default(S1))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(null)); + + System.Console.Write(' '); + System.Console.Write(Test4(new S1(11))); + System.Console.Write(Test4(default(S1))); + System.Console.Write(Test4(new S1(""11""))); + System.Console.Write(Test4(null)); + } + + static bool Test1(S1? u) + { + return u is (10); + } + + static bool Test2(S1? u) + { + return u is (10 or 11); + } + + static bool Test3(S1? u) + { + return u is (""11"" and ['1', '1']); + } + + static bool Test4(S1? u) + { + return u is (null); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalseFalse TrueFalseFalseFalseTrueFalse FalseFalseTrueFalse FalseTrueFalseTrue" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalseFalse TrueFalseFalseFalseTrueFalse FalseFalseTrueFalse FalseTrueFalseTrue" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (44,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (10); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "(10)").WithArguments("unions").WithLocation(44, 21), + // (49,22): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (10 or 11); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(49, 22), + // (49,28): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (10 or 11); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "11").WithArguments("unions").WithLocation(49, 28), + // (54,22): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is ("11" and ['1', '1']); + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"""11""").WithArguments("unions").WithLocation(54, 22), + // (59,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (null); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "(null)").WithArguments("unions").WithLocation(59, 21) + ); + } + + [Fact] + public void UnionMatching_24_Relational_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + } + + static bool Test1(S1 u) + { + return u is >=10; + } + + static bool Test2(S1 u) + { + return u is <10 or 11; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalse FalseFalseFalseTrueTrue").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalse FalseFalseFalseTrueTrue").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (30,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is >=10; + Diagnostic(ErrorCode.ERR_FeatureInPreview, ">=10").WithArguments("unions").WithLocation(30, 21), + // (35,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is <10 or 11; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "<10").WithArguments("unions").WithLocation(35, 21), + // (35,28): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is <10 or 11; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "11").WithArguments("unions").WithLocation(35, 28) + ); + } + + [Fact] + public void UnionMatching_24_Relational_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(new S1(11))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1? u) + { + return u is >=10; + } + + static bool Test2(S1? u) + { + return u is <10 or 11; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseFalse FalseFalseFalseTrueTrueFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseFalse FalseFalseFalseTrueTrueFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (32,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is >=10; + Diagnostic(ErrorCode.ERR_FeatureInPreview, ">=10").WithArguments("unions").WithLocation(32, 21), + // (37,21): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is <10 or 11; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "<10").WithArguments("unions").WithLocation(37, 21), + // (37,28): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is <10 or 11; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "11").WithArguments("unions").WithLocation(37, 28) + ); + } + + [Fact] + public void UnionMatching_25_List() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int[] x) { _value = x; } + public S1(string[] x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static bool Test1(S1 u) + { +#line 14 + return u is [10]; + } + + static bool Test2(S1? u) + { +#line 19 + return u is [10]; + } +} + +static class Extensions +{ + extension(object o) + { + public int Length => 0; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + + comp.VerifyDiagnostics( + // (14,21): error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found. + // return u is [10]; + Diagnostic(ErrorCode.ERR_ListPatternRequiresLength, "[10]").WithArguments("object").WithLocation(14, 21), + // (14,21): error CS0021: Cannot apply indexing with [] to an expression of type 'object' + // return u is [10]; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "[10]").WithArguments("object").WithLocation(14, 21), + // (19,21): error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found. + // return u is [10]; + Diagnostic(ErrorCode.ERR_ListPatternRequiresLength, "[10]").WithArguments("object").WithLocation(19, 21), + // (19,21): error CS0021: Cannot apply indexing with [] to an expression of type 'object' + // return u is [10]; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "[10]").WithArguments("object").WithLocation(19, 21) + ); + } + + [Fact] + public void UnionMatching_26_List_Subpattern_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +struct S2 +{ + private S1 _value; + public S2(S1 x) {_value = x;} + public int Length => 2; + public S1 this[int i] => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S2(new S1(10)))); + System.Console.Write(Test1(new S2(default))); + System.Console.Write(Test1(new S2(new S1(""11"")))); + System.Console.Write(Test1(new S2(new S1(0)))); + } + + static bool Test1(S2 u) + { + return u is [10, _]; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (31,22): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is [10, _]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(31, 22) + ); + } + + [Fact] + public void UnionMatching_26_List_Subpattern_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +struct S2 +{ + private S1? _value; + public S2(S1? x) {_value = x;} + public int Length => 2; + public S1? this[int i] => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S2(new S1(10)))); + System.Console.Write(Test1(new S2(default(S1)))); + System.Console.Write(Test1(new S2(new S1(""11"")))); + System.Console.Write(Test1(new S2(new S1(0)))); + System.Console.Write(Test1(new S2(null))); + } + + static bool Test1(S2 u) + { + return u is [10, _]; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_27_Slice_Subpattern_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +struct S2 +{ + private S1 _value; + public S2(S1 x) {_value = x;} + public int Length => 2; + public int this[int i] => 0; + public S1 this[System.Range r] => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S2(new S1(10)))); + System.Console.Write(Test1(new S2(default))); + System.Console.Write(Test1(new S2(new S1(""11"")))); + System.Console.Write(Test1(new S2(new S1(0)))); + } + + static bool Test1(S2 u) + { + return u is [0, ..10]; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (32,27): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is [0, ..10]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(32, 27) + ); + } + + [Fact] + public void UnionMatching_27_Slice_Subpattern_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +struct S2 +{ + private S1? _value; + public S2(S1? x) {_value = x;} + public int Length => 2; + public int this[int i] => 0; + public S1? this[System.Range r] => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S2(new S1(10)))); + System.Console.Write(Test1(new S2(default(S1)))); + System.Console.Write(Test1(new S2(new S1(""11"")))); + System.Console.Write(Test1(new S2(new S1(0)))); + System.Console.Write(Test1(new S2(null))); + } + + static bool Test1(S2 u) + { + return u is [0, ..10]; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_28_Tuple_Deconstruction_Subpattern() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1((new S1(10), -1))); + System.Console.Write(Test1((default, -1))); + System.Console.Write(Test1((new S1(""11""), -1))); + System.Console.Write(Test1((new S1(0), -1))); + System.Console.Write(' '); + System.Console.Write(Test2((new S1(10), -1))); + System.Console.Write(Test2((default(S1), -1))); + System.Console.Write(Test2((new S1(""11""), -1))); + System.Console.Write(Test2((new S1(0), -1))); + System.Console.Write(Test2((null, -1))); + } + + static bool Test1((S1, int) u) + { + return u is (10, _); + } + + static bool Test2((S1?, int) u) + { + return u is (10, _); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse TrueFalseFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (29,22): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (10, _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(29, 22), + // (34,22): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (10, _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(34, 22) + ); + } + + [Fact] + public void UnionMatching_29_ITuple_Deconstruction_Subpattern() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C(new S1(10)))); + System.Console.Write(Test1(new C(default))); + System.Console.Write(Test1(new C(new S1(11)))); + System.Console.Write(Test1(new C(new S1(""10"")))); + } + + static bool Test1(C u) + { + return u is (S1 and 10, _); + } +} + +class C : System.Runtime.CompilerServices.ITuple +{ + private readonly S1 _value; + public C(S1 x) { _value = x; } + int System.Runtime.CompilerServices.ITuple.Length => 2; + object System.Runtime.CompilerServices.ITuple.this[int i] => _value; +} + +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TrueFalseFalseFalse" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (23,29): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (S1 and 10, _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(23, 29) + ); + } + + [Fact] + public void UnionMatching_30_Deconstruction_Subpattern_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C(new S1(10)))); + System.Console.Write(Test1(new C(default))); + System.Console.Write(Test1(new C(new S1(11)))); + System.Console.Write(Test1(new C(new S1(""10"")))); + } + + static bool Test1(C u) + { + return u is (10, _); + } +} + +class C +{ + private readonly S1 _value; + public C(S1 x) { _value = x; } + public void Deconstruct(out S1 a, out int b) { a = _value; b = -1; } +} + +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (23,22): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is (10, _); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(23, 22) + ); + } + + [Fact] + public void UnionMatching_30_Deconstruction_Subpattern_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C(new S1(10)))); + System.Console.Write(Test1(new C(default(S1)))); + System.Console.Write(Test1(new C(new S1(11)))); + System.Console.Write(Test1(new C(new S1(""10"")))); + System.Console.Write(Test1(new C(null))); + } + + static bool Test1(C u) + { + return u is (10, _); + } +} + +class C +{ + private readonly S1? _value; + public C(S1? x) { _value = x; } + public void Deconstruct(out S1? a, out int b) { a = _value; b = -1; } +} + +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseFalse").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_31_Property_Subpattern_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C(new S1(10)))); + System.Console.Write(Test1(new C(default))); + System.Console.Write(Test1(new C(new S1(11)))); + System.Console.Write(Test1(new C(new S1(""10"")))); + } + + static bool Test1(C u) + { + return u is { P: 10 }; + } +} + +class C +{ + private readonly S1 _value; + public C(S1 x) { _value = x; } + public S1 P => _value; +} + +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalse").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (23,26): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is { P: 10 }; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(23, 26) + ); + } + + [Fact] + public void UnionMatching_31_Property_Subpattern_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C(new S1(10)))); + System.Console.Write(Test1(new C(default(S1)))); + System.Console.Write(Test1(new C(new S1(11)))); + System.Console.Write(Test1(new C(new S1(""10"")))); + System.Console.Write(Test1(new C(null))); + } + + static bool Test1(C u) + { + return u is { P: 10 }; + } +} + +class C +{ + private readonly S1? _value; + public C(S1? x) { _value = x; } + public S1? P => _value; +} + +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseFalse").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_32_Negated_Subpattern() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(11))); + System.Console.Write(Test1(new S1(""10""))); + System.Console.Write(Test1(null)); + } + + static bool Test1(object u) + { + return u is not (S1 and 10); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "FalseTrueTrueTrueTrue").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "FalseTrueTrueTrueTrue").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (24,33): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return u is not (S1 and 10); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(24, 33) + ); + } + + [Fact] + public void UnionMatching_33_SwitchLabel_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default)); + System.Console.Write(Test2(new S1(""10""))); + System.Console.Write(Test2(new S1(0))); + } + + static bool Test1(S1 u) + { + switch (u) + { + case int: return true; + default: return false; + } + } + + static bool Test2(S1 u) + { + switch (u) + { + case 10: return true; + default: return false; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseTrue TrueFalseFalseFalse").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_33_SwitchLabel_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(default(S1))); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(""10""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1? u) + { + switch (u) + { + case int: return true; + default: return false; + } + } + + static bool Test2(S1? u) + { + switch (u) + { + case 10: return true; + default: return false; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseTrueFalse TrueFalseFalseFalseFalse").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_34_BinaryAnd() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S0 +{ + private readonly object _value; + public S0(S1 x) { _value = x; } + public S0(string x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + private readonly object _value; + public S2(int x) { _value = x; } + public S2(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S0(new S1(new S2(10))))); + System.Console.Write(Test2(new S0(new S1(new S2(11))))); + System.Console.Write(Test2(new S0(null))); + System.Console.Write(Test2(new S0(new S1()))); + System.Console.Write(Test2(new S0(new S1(new S2())))); + System.Console.Write(Test2(new S0(new S1(""11"")))); + System.Console.Write(Test2(new S0(new S1(new S2(""11""))))); + + System.Console.Write(' '); + System.Console.Write(Test3(new S0(new S1(new S2(10))))); + System.Console.Write(Test3(new S0(new S1(new S2(11))))); + System.Console.Write(Test3(new S0(null))); + System.Console.Write(Test3(new S0(new S1()))); + System.Console.Write(Test3(new S0(new S1(new S2())))); + System.Console.Write(Test3(new S0(new S1(""11"")))); + System.Console.Write(Test3(new S0(new S1(new S2(""11""))))); + } + + static bool Test2(S0 u) + { + return u is S1 and S2 and 10; + } + + static bool Test3(S0 u) + { + return u is S1 and (S2 and 10); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseFalseFalseFalse TrueFalseFalseFalseFalseFalseFalse").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_35_TypeParameter() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object _value; + public C1(int x) { _value = x; } + public C1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C1(1))); + System.Console.Write(Test2(new C1(""2""))); + System.Console.Write(Test3(new C1(3))); + System.Console.Write(Test4(new C1(4))); + } + + static bool Test1(T u) where T : C1 + { + return u is int; + } + + static bool Test2(T u) where T : C1 + { + return u is string; + } + + static bool Test3(T u) where T : C1 + { + return u is long; + } + + static bool Test4(T u) where T : C1 + { + return u is C1; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + var verifier = CompileAndVerify(comp, expectedOutput: "FalseFalseFalseTrue").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1(T)", @" +{ + // Code size 15 (0xf) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: box ""T"" + IL_0006: isinst ""int"" + IL_000b: ldnull + IL_000c: cgt.un + IL_000e: ret +} +"); + + verifier.VerifyIL("Program.Test2(T)", @" +{ + // Code size 15 (0xf) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: box ""T"" + IL_0006: isinst ""string"" + IL_000b: ldnull + IL_000c: cgt.un + IL_000e: ret +} +"); + + verifier.VerifyIL("Program.Test3(T)", @" +{ + // Code size 15 (0xf) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: box ""T"" + IL_0006: isinst ""long"" + IL_000b: ldnull + IL_000c: cgt.un + IL_000e: ret +} +"); + + verifier.VerifyIL("Program.Test4(T)", @" +{ + // Code size 10 (0xa) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: box ""T"" + IL_0006: ldnull + IL_0007: cgt.un + IL_0009: ret +} +"); + } + + [Fact] + public void UnionMatching_36_SwitchStatement() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(new S1(""10""))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(11))); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(new S1(""10""))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(11))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(null)); + } + + static int Test1(S1 u) + { + switch (u) + { + case 10: return 1; + case ""11"": return 2; + } + + return -1; + } + + static int Test2(S1? u) + { + switch (u) + { + case 10: return 1; + case ""11"": return 2; + } + + return -1; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "1-1-1-12-1 1-1-1-12-1-1").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "1-1-1-12-1 1-1-1-12-1-1").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (35,18): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // case 10: return 1; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(35, 18), + // (36,18): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // case "11": return 2; + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"""11""").WithArguments("unions").WithLocation(36, 18), + // (46,18): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // case 10: return 1; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(46, 18), + // (47,18): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // case "11": return 2; + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"""11""").WithArguments("unions").WithLocation(47, 18) + ); + } + + [Fact] + public void UnionMatching_37_SwitchStatement_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(Test1(new S1(""10""))); + System.Console.Write(Test1(default)); + System.Console.Write(Test1(new S1(11))); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(new S1(""10""))); + System.Console.Write(Test2(default(S1))); + System.Console.Write(Test2(new S1(11))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(null)); + System.Console.Write(' '); + System.Console.Write(Test3(new S1(10))); + System.Console.Write(Test3(new S1(""10""))); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(new S1(0))); + } + + static int Test1(S1 u) + { + switch (u) + { + case null: return 66; + case 10: goto case 44; + case ""11"": goto case ""55""; + case 44: return 44; + case ""55"": return 55; + case 11: goto case null; + } + + return -1; + } + + static int Test2(S1? u) + { + switch (u) + { + case null: return 66; + case 10: goto case 44; + case ""11"": goto case ""55""; + case 44: return 44; + case ""55"": return 55; + } + + return -1; + } + + static int Test3(S1? u) + { + switch (u) + { + case null: return 66; + case 10: goto case null; + case ""11"": goto case ""55""; + case ""55"": return 55; + } + + return -1; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "44-1666655-1 44-166-155-166 66-1-155-1").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_37_SwitchStatement_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(new S1(""10""))); + System.Console.Write(Test2(new S1(11))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(null)); + System.Console.Write(' '); + System.Console.Write(Test3(new S1(10))); + System.Console.Write(Test3(new S1(""10""))); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(new S1(0))); + } + + static int Test2(S1 u) + { + switch (u) + { + case null: return 66; + case 10: goto case 44; + case ""11"": goto case ""55""; + case 44: return 44; + case ""55"": return 55; + } + + return -1; + } + + static int Test3(S1 u) + { + switch (u) + { + case null: return 66; + case 10: goto case null; + case ""11"": goto case ""55""; + case ""55"": return 55; + } + + return -1; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "44-1-155-166 66-1-155-1").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_37_SwitchStatement_03() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(S1 x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1(10))); + System.Console.Write(Test2(new S1(""10""))); + System.Console.Write(Test2(new S1(11))); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(Test2(null)); + System.Console.Write(' '); + System.Console.Write(Test3(new S1(10))); + System.Console.Write(Test3(new S1(""10""))); + System.Console.Write(Test3(new S1(11))); + System.Console.Write(Test3(new S1(""11""))); + System.Console.Write(Test3(new S1(0))); + } + + const S1 _S1_null = null; + + static int Test2(S1 u) + { + switch (u) + { + case _S1_null: return 66; + case 10: goto case 44; + case ""11"": goto case ""55""; + case 44: return 44; + case ""55"": return 55; + } + + return -1; + } + + static int Test3(S1 u) + { + switch (u) + { + case _S1_null: return 66; + case 10: goto case _S1_null; + case ""11"": goto case ""55""; + case ""55"": return 55; + } + + return -1; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "44-1-155-166 66-1-155-1").VerifyDiagnostics(); + } + + [Fact] + public void UnionMatching_38_SwitchStatement() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static int Test1(S1 u) + { + switch (u) + { + case 10: return 1; + case ""11"": return 2; +#line 18 + case true: return 3; + } + + return -1; + } + + static int Test2(S1? u) + { + switch (u) + { + case 10: return 1; + case ""11"": return 2; +#line 30 + case true: return 3; + } + + return -1; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (18,18): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // case true: return 3; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "true").WithArguments("S1").WithLocation(18, 18), + // (18,18): error CS0029: Cannot implicitly convert type 'bool' to 'int' + // case true: return 3; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "true").WithArguments("bool", "int").WithLocation(18, 18), + // (18,18): error CS0029: Cannot implicitly convert type 'bool' to 'string' + // case true: return 3; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "true").WithArguments("bool", "string").WithLocation(18, 18), + // (30,18): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // case true: return 3; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "true").WithArguments("S1").WithLocation(30, 18), + // (30,18): error CS0029: Cannot implicitly convert type 'bool' to 'int' + // case true: return 3; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "true").WithArguments("bool", "int").WithLocation(30, 18), + // (30,18): error CS0029: Cannot implicitly convert type 'bool' to 'string' + // case true: return 3; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "true").WithArguments("bool", "string").WithLocation(30, 18) + ); + } + + [Fact] + public void UnionMatching_39_SwitchStatement() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static int Test1(S1 u) + { + switch (u) + { +#line 16 + case 10: goto case true; + case ""11"": return 2; + } + + return -1; + } + + static int Test2(S1? u) + { + switch (u) + { +#line 27 + case 10: goto case true; + case ""11"": return 2; + } + + return -1; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (16,13): error CS0163: Control cannot fall through from one case label ('case 10:') to another + // case 10: goto case true; + Diagnostic(ErrorCode.ERR_SwitchFallThrough, "case 10:").WithArguments("case 10:").WithLocation(16, 13), + // (16,22): error CS0029: Cannot implicitly convert type 'bool' to 'S1' + // case 10: goto case true; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "goto case true;").WithArguments("bool", "S1").WithLocation(16, 22), + // (27,13): error CS0163: Control cannot fall through from one case label ('case 10:') to another + // case 10: goto case true; + Diagnostic(ErrorCode.ERR_SwitchFallThrough, "case 10:").WithArguments("case 10:").WithLocation(27, 13), + // (27,22): error CS0029: Cannot implicitly convert type 'bool' to 'S1?' + // case 10: goto case true; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "goto case true;").WithArguments("bool", "S1?").WithLocation(27, 22) + ); + } + + [Fact] + public void PatternWrongType_TypePattern_01_BindConstantPatternWithFallbackToTypePattern_UnionType_Out_UnionType_In() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { +#line 100 + _ = u is C1 and C2; + _ = u is C1 and C3; + _ = u is C1 and C4; + _ = u switch { C4 => 1, _ => 0 }; + } + + static void Test2(S1? u) + { +#line 200 + _ = u is C1 and C2; + _ = u is C1 and C3; + _ = u is C1 and C4; + _ = u switch { C4 => 1, _ => 0 }; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,25): hidden CS9335: The pattern is redundant. + // _ = u is C1 and C2; + Diagnostic(ErrorCode.HDN_RedundantPattern, "C2").WithLocation(100, 25), + // (101,25): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(101, 25), + // (102,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(102, 25), + // (103,24): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u switch { C4 => 1, _ => 0 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(103, 24), + // (200,25): hidden CS9335: The pattern is redundant. + // _ = u is C1 and C2; + Diagnostic(ErrorCode.HDN_RedundantPattern, "C2").WithLocation(200, 25), + // (201,25): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(201, 25), + // (202,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(202, 25), + // (203,24): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u switch { C4 => 1, _ => 0 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(203, 24) + ); + } + + [Fact] + public void PatternWrongType_TypePattern_02_BindTypePattern_UnionType_Out_UnionType_In() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test4(S1 u) + { +#line 400 + _ = u is System.IComparable and string; + _ = u is string and int; + _ = u is object and byte; + _ = u switch { byte => 1, _ => 0 }; + } + + static void Test5(S1? u) + { +#line 500 + _ = u is System.IComparable and string; + _ = u is string and int; + _ = u is object and byte; + _ = u switch { byte => 1, _ => 0 }; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (401,29): error CS8121: An expression of type 'string' cannot be handled by a pattern of type 'int'. + // _ = u is string and int; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("string", "int").WithLocation(401, 29), + // (402,29): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'byte'. + // _ = u is object and byte; + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("S1", "byte").WithLocation(402, 29), + // (403,24): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'byte'. + // _ = u switch { byte => 1, _ => 0 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("S1", "byte").WithLocation(403, 24), + // (501,29): error CS8121: An expression of type 'string' cannot be handled by a pattern of type 'int'. + // _ = u is string and int; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("string", "int").WithLocation(501, 29), + // (502,29): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'byte'. + // _ = u is object and byte; + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("S1", "byte").WithLocation(502, 29), + // (503,24): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'byte'. + // _ = u switch { byte => 1, _ => 0 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("S1", "byte").WithLocation(503, 24) + ); + } + + [Fact] + public void PatternWrongType_TypePattern_03() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test4(S1 u) + { +#line 400 + switch (u) + { + case string: + break; + case byte: + break; + } + } + + static void Test5(S1? u) + { +#line 500 + switch (u) + { + case string: + break; + case byte: + break; + } + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (404,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'byte'. + // case byte: + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("S1", "byte").WithLocation(404, 18), + // (504,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'byte'. + // case byte: + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("S1", "byte").WithLocation(504, 18) + ); + } + + [Fact] + public void PatternWrongType_TypePattern_04_BindIsOperator() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; +} +"; + var src2 = @" +class Program +{ + static void Test4(S1 u) + { + _ = u is System.IComparable; + _ = u is int; + _ = u is string; + _ = u is object; +#line 400 + _ = u is long; + } + + static void Test5(S1? u) + { + _ = u is System.IComparable; + _ = u is int; + _ = u is string; + _ = u is object; +#line 500 + _ = u is long; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (400,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'long'. + // _ = u is long; + Diagnostic(ErrorCode.ERR_PatternWrongType, "long").WithArguments("S1", "long").WithLocation(400, 18), + // (500,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'long'. + // _ = u is long; + Diagnostic(ErrorCode.ERR_PatternWrongType, "long").WithArguments("S1", "long").WithLocation(500, 18) + ); + } + + [Fact] + public void PatternWrongType_RecursivePattern_01_BindRecursivePattern_UnionType_In() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test2(S1 u) + { +#line 200 + _ = u is C1 and C2 {}; + _ = u is C1 and C3 {}; + _ = u is C1 and C4 {}; + _ = u is C4 {}; + } + + static void Test3(S1? u) + { +#line 300 + _ = u is C1 and C2 {}; + _ = u is C1 and C3 {}; + _ = u is C1 and C4 {}; + _ = u is C4 {}; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (200,25): hidden CS9335: The pattern is redundant. + // _ = u is C1 and C2 {}; + Diagnostic(ErrorCode.HDN_RedundantPattern, "C2 {}").WithLocation(200, 25), + // (201,25): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and C3 {}; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(201, 25), + // (202,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and C4 {}; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(202, 25), + // (203,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C4 {}; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(203, 18), + // (300,25): hidden CS9335: The pattern is redundant. + // _ = u is C1 and C2 {}; + Diagnostic(ErrorCode.HDN_RedundantPattern, "C2 {}").WithLocation(300, 25), + // (301,25): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and C3 {}; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(301, 25), + // (302,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and C4 {}; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(302, 25), + // (303,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C4 {}; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(303, 18) + ); + } + + [Fact] + public void PatternWrongType_RecursivePattern_02_BindRecursivePattern_UnionType_Out() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test10(S1 u) + { +#line 1000 + _ = u is C1 {} and C2; + _ = u is C1 {} and C3; + _ = u is C1 {} and C4; + } + + static void Test20(S1? u) + { +#line 2000 + _ = u is C1 {} and C2; + _ = u is C1 {} and C3; + _ = u is C1 {} and C4; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (1000,28): hidden CS9335: The pattern is redundant. + // _ = u is C1 {} and C2; + Diagnostic(ErrorCode.HDN_RedundantPattern, "C2").WithLocation(1000, 28), + // (1001,28): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 {} and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(1001, 28), + // (1002,28): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 {} and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(1002, 28), + // (2000,28): hidden CS9335: The pattern is redundant. + // _ = u is C1 {} and C2; + Diagnostic(ErrorCode.HDN_RedundantPattern, "C2").WithLocation(2000, 28), + // (2001,28): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 {} and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(2001, 28), + // (2002,28): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 {} and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(2002, 28) + ); + } + + [Fact] + public void PatternWrongType_DeclarationPattern_01_BindDeclarationPattern_UnionType_In() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test3(S1 u) + { +#line 300 + _ = u is C1 and C2 a; + _ = u is C1 and C3 b; + _ = u is C1 and C4 c; + _ = u is C4 d; + } + + static void Test4(S1? u) + { +#line 400 + _ = u is C1 and C2 a; + _ = u is C1 and C3 b; + _ = u is C1 and C4 c; + _ = u is C4 d; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (301,25): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and C3 b; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(301, 25), + // (302,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and C4 c; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(302, 25), + // (303,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C4 d; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(303, 18), + // (401,25): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and C3 b; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(401, 25), + // (402,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and C4 c; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(402, 25), + // (403,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C4 d; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(403, 18) + ); + } + + [Fact] + public void PatternWrongType_DeclarationPattern_02_BindDeclarationPattern_UnionType_Out() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test9(S1 u) + { +#line 900 + _ = u is C1 a and C2; + _ = u is C1 b and C3; + _ = u is C1 c and C4; + } + + static void Test10(S1? u) + { +#line 950 + _ = u is C1 a and C2; + _ = u is C1 b and C3; + _ = u is C1 c and C4; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (900,27): hidden CS9335: The pattern is redundant. + // _ = u is C1 a and C2; + Diagnostic(ErrorCode.HDN_RedundantPattern, "C2").WithLocation(900, 27), + // (901,27): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 b and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(901, 27), + // (902,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 c and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(902, 27), + // (950,27): hidden CS9335: The pattern is redundant. + // _ = u is C1 a and C2; + Diagnostic(ErrorCode.HDN_RedundantPattern, "C2").WithLocation(950, 27), + // (951,27): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 b and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(951, 27), + // (952,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 c and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(952, 27) + ); + } + + [Fact] + public void PatternWrongType_NegatedPattern_01_BindUnaryPattern_UnionType_Out() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test5(S1 u) + { +#line 500 + _ = u is not C5 and C2; + _ = u is not C5 and C4; + } + + static void Test6(S1? u) + { +#line 600 + _ = u is not C5 and C2; + _ = u is not C5 and C4; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (501,29): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is not C5 and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(501, 29), + // (601,29): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is not C5 and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(601, 29) + ); + } + + [Fact] + public void PatternWrongType_NegatedPattern_02_BindUnaryPattern_UnionType_In() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test7(S1 u) + { +#line 700 + _ = u is C1 and not C5; + _ = u is C1 and not C3; + _ = u is C1 and not C4; + _ = u is not C4; + } + + static void Test8(S1? u) + { +#line 800 + _ = u is C1 and not C5; + _ = u is C1 and not C3; + _ = u is C1 and not C4; + _ = u is not C4; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (701,29): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and not C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(701, 29), + // (702,29): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and not C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(702, 29), + // (703,22): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is not C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(703, 22), + // (801,29): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and not C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(801, 29), + // (802,29): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and not C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(802, 29), + // (803,22): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is not C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(803, 22) + ); + } + + [Fact] + public void PatternWrongType_ParenthesizedPattern_01_BindParenthesizedPattern_UnionType_Out() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test6(S1 u) + { +#line 600 + _ = u is (not C5) and C2; + _ = u is (not C5) and C4; + } + + static void Test7(S1? u) + { +#line 700 + _ = u is (not C5) and C2; + _ = u is (not C5) and C4; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (601,31): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is (not C5) and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(601, 31), + // (701,31): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is (not C5) and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(701, 31) + ); + } + + [Fact] + public void PatternWrongType_ParenthesizedPattern_01_BindParenthesizedPattern_UnionType_In() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test8(S1 u) + { +#line 800 + _ = u is C1 and (not C2); + _ = u is C1 and (not C3); + _ = u is C1 and (not C4); + _ = u is (not C4); + } + + static void Test9(S1? u) + { +#line 900 + _ = u is C1 and (not C2); + _ = u is C1 and (not C3); + _ = u is C1 and (not C4); + _ = u is (not C4); + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (801,30): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and (not C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(801, 30), + // (802,30): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and (not C4); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(802, 30), + // (803,23): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is (not C4); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(803, 23), + // (901,30): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 and (not C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("C1", "C3").WithLocation(901, 30), + // (902,30): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is C1 and (not C4); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(902, 30), + // (903,23): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is (not C4); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(903, 23) + ); + } + + [Fact] + public void PatternWrongType_ListPattern_01_BindListPattern_UnionType_Out() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test11(S1 u) + { +#line 1100 + _ = u is [] and C2; + _ = u is [] and C4; + _ = u is string and ['a']; + } + + static void Test21(S1? u) + { +#line 2100 + _ = u is [] and C2; + _ = u is [] and C4; + _ = u is string and ['a']; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (1100,18): error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found. + // _ = u is [] and C2; + Diagnostic(ErrorCode.ERR_ListPatternRequiresLength, "[]").WithArguments("object").WithLocation(1100, 18), + // (1100,18): error CS0021: Cannot apply indexing with [] to an expression of type 'object' + // _ = u is [] and C2; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "[]").WithArguments("object").WithLocation(1100, 18), + // (1101,18): error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found. + // _ = u is [] and C4; + Diagnostic(ErrorCode.ERR_ListPatternRequiresLength, "[]").WithArguments("object").WithLocation(1101, 18), + // (1101,18): error CS0021: Cannot apply indexing with [] to an expression of type 'object' + // _ = u is [] and C4; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "[]").WithArguments("object").WithLocation(1101, 18), + // (1101,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is [] and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(1101, 25), + // (2100,18): error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found. + // _ = u is [] and C2; + Diagnostic(ErrorCode.ERR_ListPatternRequiresLength, "[]").WithArguments("object").WithLocation(2100, 18), + // (2100,18): error CS0021: Cannot apply indexing with [] to an expression of type 'object' + // _ = u is [] and C2; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "[]").WithArguments("object").WithLocation(2100, 18), + // (2101,18): error CS8985: List patterns may not be used for a value of type 'object'. No suitable 'Length' or 'Count' property was found. + // _ = u is [] and C4; + Diagnostic(ErrorCode.ERR_ListPatternRequiresLength, "[]").WithArguments("object").WithLocation(2101, 18), + // (2101,18): error CS0021: Cannot apply indexing with [] to an expression of type 'object' + // _ = u is [] and C4; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "[]").WithArguments("object").WithLocation(2101, 18), + // (2101,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is [] and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(2101, 25) + ); + } + + [Fact] + public void PatternWrongType_VarDeconstructionPattern_01_UnionType_Out() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; +class C3; +class C4 : C1; +class C5 : C2; +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { +#line 100 + _ = u is var (a, b) and C2; + _ = u is var (c, d) and C4; + } + + static void Test2(S1? u) + { +#line 200 + _ = u is var (a, b) and C2; + _ = u is var (c, d) and C4; + } +} + +static class Extensions +{ + public static void Deconstruct(this object o, out int x, out int y) + { + x = 1; + y = 2; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (101,33): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is var (c, d) and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(101, 33), + // (201,33): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C4'. + // _ = u is var (c, d) and C4; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C4").WithArguments("S1", "C4").WithLocation(201, 33) + ); + } + + [Fact] + public void PatternWrongType_ConstantPattern_01_BindConstantPatternWithFallbackToTypePattern_UnionType_In_01() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C1 x) { _value = x; } + public object Value => _value; +} + +class C1 +{ + public static implicit operator C1(string c) => null; +} + +class C2; +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { +#line 100 + _ = u is {} and ""1""; + _ = u is C1 and (C2)null; + _ = u is C1 and ""1""; + _ = u is System.IComparable and ""1""; + _ = u is ""1""; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,25): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is {} and "1"; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, @"""1""").WithArguments("S1").WithLocation(100, 25), + // (100,25): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and "1"; + Diagnostic(ErrorCode.ERR_PatternWrongType, @"""1""").WithArguments("C1", "string").WithLocation(100, 25), + // (100,25): error CS0029: Cannot implicitly convert type 'string' to 'int' + // _ = u is {} and "1"; + Diagnostic(ErrorCode.ERR_NoImplicitConv, @"""1""").WithArguments("string", "int").WithLocation(100, 25), + // (101,25): error CS0029: Cannot implicitly convert type 'C2' to 'C1' + // _ = u is C1 and (C2)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(C2)null").WithArguments("C2", "C1").WithLocation(101, 25), + // (102,25): error CS9135: A constant value of type 'C1' is expected + // _ = u is C1 and "1"; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, @"""1""").WithArguments("C1").WithLocation(102, 25), + // (103,41): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is System.IComparable and "1"; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, @"""1""").WithArguments("S1").WithLocation(103, 41), + // (103,41): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'string'. + // _ = u is System.IComparable and "1"; + Diagnostic(ErrorCode.ERR_PatternWrongType, @"""1""").WithArguments("C1", "string").WithLocation(103, 41), + // (103,41): error CS0029: Cannot implicitly convert type 'string' to 'int' + // _ = u is System.IComparable and "1"; + Diagnostic(ErrorCode.ERR_NoImplicitConv, @"""1""").WithArguments("string", "int").WithLocation(103, 41), + // (104,18): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is "1"; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, @"""1""").WithArguments("S1").WithLocation(104, 18), + // (104,18): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'string'. + // _ = u is "1"; + Diagnostic(ErrorCode.ERR_PatternWrongType, @"""1""").WithArguments("C1", "string").WithLocation(104, 18), + // (104,18): error CS0029: Cannot implicitly convert type 'string' to 'int' + // _ = u is "1"; + Diagnostic(ErrorCode.ERR_NoImplicitConv, @"""1""").WithArguments("string", "int").WithLocation(104, 18) + ); + } + + [Fact] + public void PatternWrongType_ConstantPattern_01_BindConstantPatternWithFallbackToTypePattern_UnionType_In_02() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C1 x) { _value = x; } + public object Value => _value; +} + +class C1 +{ + public static implicit operator C1(string c) => null; +} + +class C2; +"; + var src2 = @" +class Program +{ + static void Test1(S1? u) + { +#line 100 + _ = u is {} and ""1""; + _ = u is C1 and (C2)null; + _ = u is C1 and ""1""; + _ = u is System.IComparable and ""1""; + _ = u is ""1""; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,25): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is {} and "1"; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, @"""1""").WithArguments("S1").WithLocation(100, 25), + // (100,25): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and "1"; + Diagnostic(ErrorCode.ERR_PatternWrongType, @"""1""").WithArguments("C1", "string").WithLocation(100, 25), + // (100,25): error CS0029: Cannot implicitly convert type 'string' to 'int' + // _ = u is {} and "1"; + Diagnostic(ErrorCode.ERR_NoImplicitConv, @"""1""").WithArguments("string", "int").WithLocation(100, 25), + // (101,25): error CS0029: Cannot implicitly convert type 'C2' to 'C1' + // _ = u is C1 and (C2)null; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "(C2)null").WithArguments("C2", "C1").WithLocation(101, 25), + // (102,25): error CS9135: A constant value of type 'C1' is expected + // _ = u is C1 and "1"; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, @"""1""").WithArguments("C1").WithLocation(102, 25), + // (103,41): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is System.IComparable and "1"; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, @"""1""").WithArguments("S1").WithLocation(103, 41), + // (103,41): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'string'. + // _ = u is System.IComparable and "1"; + Diagnostic(ErrorCode.ERR_PatternWrongType, @"""1""").WithArguments("C1", "string").WithLocation(103, 41), + // (103,41): error CS0029: Cannot implicitly convert type 'string' to 'int' + // _ = u is System.IComparable and "1"; + Diagnostic(ErrorCode.ERR_NoImplicitConv, @"""1""").WithArguments("string", "int").WithLocation(103, 41), + // (104,18): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is "1"; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, @"""1""").WithArguments("S1").WithLocation(104, 18), + // (104,18): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'string'. + // _ = u is "1"; + Diagnostic(ErrorCode.ERR_PatternWrongType, @"""1""").WithArguments("C1", "string").WithLocation(104, 18), + // (104,18): error CS0029: Cannot implicitly convert type 'string' to 'int' + // _ = u is "1"; + Diagnostic(ErrorCode.ERR_NoImplicitConv, @"""1""").WithArguments("string", "int").WithLocation(104, 18) + ); + } + + [Fact] + public void PatternWrongType_ConstantPattern_02_BindConstantPatternWithFallbackToTypePattern_UnionType_Out() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(byte x) { _value = x; } + public object Value => _value; +} + +class C2; +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { +#line 100 + _ = u is null and C2; + _ = u is 1 and byte; + } + + static void Test2(S1? u) + { +#line 200 + _ = u is null and C2; + _ = u is 1 and byte; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C2'. + // _ = u is null and C2; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C2").WithArguments("S1", "C2").WithLocation(100, 27), + // (101,24): error CS8121: An expression of type 'int' cannot be handled by a pattern of type 'byte'. + // _ = u is 1 and byte; + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("int", "byte").WithLocation(101, 24), + // (200,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C2'. + // _ = u is null and C2; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C2").WithArguments("S1", "C2").WithLocation(200, 27), + // (201,24): error CS8121: An expression of type 'int' cannot be handled by a pattern of type 'byte'. + // _ = u is 1 and byte; + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("int", "byte").WithLocation(201, 24) + ); + } + + [Fact] + public void PatternWrongType_ConstantPattern_03_BindIsOperator_UnionType_In() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(byte x) { _value = x; } + public object Value => _value; +} +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { + const string empty =""""; +#line 100 + _ = u is empty; + } + + static void Test2(S1? u) + { + const string empty =""""; +#line 200 + _ = u is empty; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,18): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is empty; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "empty").WithArguments("S1").WithLocation(100, 18), + // (100,18): error CS0029: Cannot implicitly convert type 'string' to 'int' + // _ = u is empty; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "empty").WithArguments("string", "int").WithLocation(100, 18), + // (100,18): error CS0029: Cannot implicitly convert type 'string' to 'byte' + // _ = u is empty; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "empty").WithArguments("string", "byte").WithLocation(100, 18), + // (200,18): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is empty; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "empty").WithArguments("S1").WithLocation(200, 18), + // (200,18): error CS0029: Cannot implicitly convert type 'string' to 'int' + // _ = u is empty; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "empty").WithArguments("string", "int").WithLocation(200, 18), + // (200,18): error CS0029: Cannot implicitly convert type 'string' to 'byte' + // _ = u is empty; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "empty").WithArguments("string", "byte").WithLocation(200, 18) + ); + } + + [Fact] + public void PatternWrongType_ConstantPattern_04_UnionType_In() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(byte x) { _value = x; } + public object Value => _value; +} +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { + const string empty =""""; +#line 100 + switch (u) + { + case 1: + goto case empty; + } + } + + static void Test2(S1? u) + { + const string empty =""""; +#line 200 + switch (u) + { + case 1: + goto case empty; + } + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (102,13): error CS8070: Control cannot fall out of switch from final case label ('case 1:') + // case 1: + Diagnostic(ErrorCode.ERR_SwitchFallOut, "case 1:").WithArguments("case 1:").WithLocation(102, 13), + + // The following error is expected per language specification (https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/statements.md#13104-the-goto-statement): + // "if the constant_expression is not implicitly convertible (§10.2) to the governing type of the nearest enclosing switch statement, a compile-time error occurs." + + // (103,17): error CS0029: Cannot implicitly convert type 'string' to 'S1' + // goto case empty; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "goto case empty;").WithArguments("string", "S1").WithLocation(103, 17), + + // (202,13): error CS8070: Control cannot fall out of switch from final case label ('case 1:') + // case 1: + Diagnostic(ErrorCode.ERR_SwitchFallOut, "case 1:").WithArguments("case 1:").WithLocation(202, 13), + + // The following error is expected per language specification (https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/statements.md#13104-the-goto-statement): + // "if the constant_expression is not implicitly convertible (§10.2) to the governing type of the nearest enclosing switch statement, a compile-time error occurs." + + // (203,17): error CS0029: Cannot implicitly convert type 'string' to 'S1?' + // goto case empty; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "goto case empty;").WithArguments("string", "S1?").WithLocation(203, 17) + ); + } + + [Fact] + public void PatternWrongType_RelationalPattern_01_BindRelationalPattern_UnionType_In_01() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(string x) { _value = x; } + public S1(C1 x) { _value = x; } + public object Value => _value; +} + +class C1 +{ + public static implicit operator C1(int c) => null; +} + +class C2; +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { +#line 100 + _ = u is {} and > 1; + _ = u is C1 and > 1; + _ = u is System.IComparable and > 1; + _ = u is > 1; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,27): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is {} and > 1; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "1").WithArguments("S1").WithLocation(100, 27), + // (100,27): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and > 1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "1").WithArguments("C1", "int").WithLocation(100, 27), + // (100,27): error CS0029: Cannot implicitly convert type 'int' to 'string' + // _ = u is {} and > 1; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "1").WithArguments("int", "string").WithLocation(100, 27), + // (101,27): error CS9135: A constant value of type 'C1' is expected + // _ = u is C1 and > 1; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "1").WithArguments("C1").WithLocation(101, 27), + // (102,43): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is System.IComparable and > 1; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "1").WithArguments("S1").WithLocation(102, 43), + // (102,43): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'int'. + // _ = u is System.IComparable and > 1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "1").WithArguments("C1", "int").WithLocation(102, 43), + // (102,43): error CS0029: Cannot implicitly convert type 'int' to 'string' + // _ = u is System.IComparable and > 1; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "1").WithArguments("int", "string").WithLocation(102, 43), + // (103,20): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is > 1; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "1").WithArguments("S1").WithLocation(103, 20), + // (103,20): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'int'. + // _ = u is > 1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "1").WithArguments("C1", "int").WithLocation(103, 20), + // (103,20): error CS0029: Cannot implicitly convert type 'int' to 'string' + // _ = u is > 1; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "1").WithArguments("int", "string").WithLocation(103, 20) + ); + } + + [Fact] + public void PatternWrongType_RelationalPattern_01_BindRelationalPattern_UnionType_In_02() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(string x) { _value = x; } + public S1(C1 x) { _value = x; } + public object Value => _value; +} + +class C1 +{ + public static implicit operator C1(int c) => null; +} + +class C2; +"; + var src2 = @" +class Program +{ + static void Test1(S1? u) + { +#line 100 + _ = u is {} and > 1; + _ = u is C1 and > 1; + _ = u is System.IComparable and > 1; + _ = u is > 1; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,27): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is {} and > 1; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "1").WithArguments("S1").WithLocation(100, 27), + // (100,27): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and > 1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "1").WithArguments("C1", "int").WithLocation(100, 27), + // (100,27): error CS0029: Cannot implicitly convert type 'int' to 'string' + // _ = u is {} and > 1; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "1").WithArguments("int", "string").WithLocation(100, 27), + // (101,27): error CS9135: A constant value of type 'C1' is expected + // _ = u is C1 and > 1; + Diagnostic(ErrorCode.ERR_ConstantValueOfTypeExpected, "1").WithArguments("C1").WithLocation(101, 27), + // (102,43): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is System.IComparable and > 1; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "1").WithArguments("S1").WithLocation(102, 43), + // (102,43): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'int'. + // _ = u is System.IComparable and > 1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "1").WithArguments("C1", "int").WithLocation(102, 43), + // (102,43): error CS0029: Cannot implicitly convert type 'int' to 'string' + // _ = u is System.IComparable and > 1; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "1").WithArguments("int", "string").WithLocation(102, 43), + // (103,20): error CS9372: An expression of type 'S1' cannot be handled by this pattern, see additional errors at this location. + // _ = u is > 1; + Diagnostic(ErrorCode.ERR_UnionMatchingWrongPattern, "1").WithArguments("S1").WithLocation(103, 20), + // (103,20): error CS8121: An expression of type 'C1' cannot be handled by a pattern of type 'int'. + // _ = u is > 1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "1").WithArguments("C1", "int").WithLocation(103, 20), + // (103,20): error CS0029: Cannot implicitly convert type 'int' to 'string' + // _ = u is > 1; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "1").WithArguments("int", "string").WithLocation(103, 20) + ); + } + + [Fact] + public void PatternWrongType_RelationalPattern_02_BindRelationalPattern_UnionType_Out() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(byte x) { _value = x; } + public object Value => _value; +} + +class C2; +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { +#line 100 + _ = u is > 1 and byte; + } + + static void Test2(S1? u) + { +#line 200 + _ = u is > 1 and byte; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,26): error CS8121: An expression of type 'int' cannot be handled by a pattern of type 'byte'. + // _ = u is > 1 and byte; + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("int", "byte").WithLocation(100, 26), + // (200,26): error CS8121: An expression of type 'int' cannot be handled by a pattern of type 'byte'. + // _ = u is > 1 and byte; + Diagnostic(ErrorCode.ERR_PatternWrongType, "byte").WithArguments("int", "byte").WithLocation(200, 26) + ); + } + + [Fact] + public void PatternWrongType_BinaryPattern_01_Disjunction_Snap_To_Previous_UnionType_01() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2; +class C3; +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { +#line 100 + _ = u is int or string or C3; + _ = u is int or (string or C3); + _ = u is C1 or string or C3; + _ = u is int or C2 or C3; + _ = u is int or string or C1; + _ = u is int or (C2 or C3); + _ = u is int or (string or C1); + _ = u is (int or string) or C3; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(100, 18), + // (100,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is int or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(100, 25), + // (100,35): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is int or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(100, 35), + // (101,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or (string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(101, 18), + // (101,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is int or (string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(101, 26), + // (101,36): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is int or (string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(101, 36), + // (102,24): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is C1 or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(102, 24), + // (102,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(102, 34), + // (103,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or C2 or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(103, 18), + // (103,31): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is int or C2 or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(103, 31), + // (104,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or string or C1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(104, 18), + // (104,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is int or string or C1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(104, 25), + // (105,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or (C2 or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(105, 18), + // (105,32): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is int or (C2 or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(105, 32), + // (106,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or (string or C1); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(106, 18), + // (106,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is int or (string or C1); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(106, 26), + // (107,19): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is (int or string) or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(107, 19), + // (107,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is (int or string) or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(107, 26), + // (107,37): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is (int or string) or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(107, 37) + ); + } + + [Fact] + public void PatternWrongType_BinaryPattern_01_Disjunction_Snap_To_Previous_UnionType_02() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2; +class C3; +"; + var src2 = @" +class Program +{ + static void Test1(S1? u) + { +#line 100 + _ = u is int or string or C3; + _ = u is int or (string or C3); + _ = u is C1 or string or C3; + _ = u is int or C2 or C3; + _ = u is int or string or C1; + _ = u is int or (C2 or C3); + _ = u is int or (string or C1); + _ = u is (int or string) or C3; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(100, 18), + // (100,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is int or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(100, 25), + // (100,35): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is int or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(100, 35), + // (101,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or (string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(101, 18), + // (101,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is int or (string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(101, 26), + // (101,36): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is int or (string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(101, 36), + // (102,24): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is C1 or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(102, 24), + // (102,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is C1 or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(102, 34), + // (103,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or C2 or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(103, 18), + // (103,31): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is int or C2 or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(103, 31), + // (104,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or string or C1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(104, 18), + // (104,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is int or string or C1; + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(104, 25), + // (105,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or (C2 or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(105, 18), + // (105,32): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is int or (C2 or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(105, 32), + // (106,18): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is int or (string or C1); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(106, 18), + // (106,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is int or (string or C1); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(106, 26), + // (107,19): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is (int or string) or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(107, 19), + // (107,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is (int or string) or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(107, 26), + // (107,37): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is (int or string) or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(107, 37) + ); + } + + [Fact] + public void PatternWrongType_BinaryPattern_02_Disjunction_Snap_To_Previous_UnionType_01() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2; +class C3; +"; + var src2 = @" +class Program +{ + static void Test1(S1 u) + { +#line 100 + _ = u is {} and (int or string or C3); + _ = u is {} and (int or (string or C3)); + _ = u is {} and (C1 or string or C3); + _ = u is {} and (int or C2 or C3); + _ = u is {} and (int or string or C1); + _ = u is {} and (int or (C2 or C3)); + _ = u is {} and (int or (string or C1)); + _ = u is {} and ((int or string) or C3); + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(100, 26), + // (100,33): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(100, 33), + // (100,43): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(100, 43), + // (101,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(101, 26), + // (101,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (int or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(101, 34), + // (101,44): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(101, 44), + // (102,32): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (C1 or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(102, 32), + // (102,42): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (C1 or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(102, 42), + // (103,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or C2 or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(103, 26), + // (103,39): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or C2 or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(103, 39), + // (104,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or string or C1); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(104, 26), + // (104,33): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (int or string or C1); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(104, 33), + // (105,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or (C2 or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(105, 26), + // (105,40): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or (C2 or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(105, 40), + // (106,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or (string or C1)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(106, 26), + // (106,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (int or (string or C1)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(106, 34), + // (107,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and ((int or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(107, 27), + // (107,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and ((int or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(107, 34), + // (107,45): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and ((int or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(107, 45) + ); + } + + [Fact] + public void PatternWrongType_BinaryPattern_02_Disjunction_Snap_To_Previous_UnionType_02() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2; +class C3; +"; + var src2 = @" +class Program +{ + static void Test1(S1? u) + { +#line 100 + _ = u is {} and (int or string or C3); + _ = u is {} and (int or (string or C3)); + _ = u is {} and (C1 or string or C3); + _ = u is {} and (int or C2 or C3); + _ = u is {} and (int or string or C1); + _ = u is {} and (int or (C2 or C3)); + _ = u is {} and (int or (string or C1)); + _ = u is {} and ((int or string) or C3); + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(100, 26), + // (100,33): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(100, 33), + // (100,43): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(100, 43), + // (101,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(101, 26), + // (101,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (int or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(101, 34), + // (101,44): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(101, 44), + // (102,32): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (C1 or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(102, 32), + // (102,42): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (C1 or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(102, 42), + // (103,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or C2 or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(103, 26), + // (103,39): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or C2 or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(103, 39), + // (104,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or string or C1); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(104, 26), + // (104,33): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (int or string or C1); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(104, 33), + // (105,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or (C2 or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(105, 26), + // (105,40): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or (C2 or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(105, 40), + // (106,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or (string or C1)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(106, 26), + // (106,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (int or (string or C1)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(106, 34), + // (107,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and ((int or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(107, 27), + // (107,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is {} and ((int or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(107, 34), + // (107,45): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and ((int or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(107, 45) + ); + } + + [Fact] + public void PatternWrongType_BinaryPattern_03_Disjunction_Snap_To_Previous_UnionType() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2; +class C3; +"; + var src2 = @" +class Program +{ + static void Test1(object u) + { +#line 100 + _ = u is (S1 and int) or string or C3; + _ = u is (S1 and int) or (string or C3); + _ = u is int or (S1 and C2) or C3; + _ = u is int or ((S1 and C2) or C3); + _ = u is ((S1 and int) or string) or C3; + _ = u is S1 and int or string or C3; + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is (S1 and int) or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(100, 26), + // (101,26): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is (S1 and int) or (string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(101, 26), + // (104,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is ((S1 and int) or string) or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(104, 27), + // (105,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is S1 and int or string or C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(105, 25) + ); + } + + [Fact] + public void PatternWrongType_BinaryPattern_04_Disjunction_Snap_To_Previous_UnionType_01() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S0 +{ + private readonly object _value; + public S0(byte x) { _value = x; } + public S0(S1 x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2; +class C3; +"; + var src2 = @" +class Program +{ + static void Test1(S0 u) + { +#line 100 + _ = u is {} and ((S1 and int) or string or C3); + _ = u is {} and ((S1 and int) or (string or C3)); + _ = u is {} and (int or (S1 and C2) or C3); + _ = u is {} and (int or ((S1 and C2) or C3)); + _ = u is {} and (((S1 and int) or string) or C3); + _ = u is {} and (S1 and int or string or C3); + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and ((S1 and int) or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(100, 34), + // (100,42): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'string'. + // _ = u is {} and ((S1 and int) or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S0", "string").WithLocation(100, 42), + // (100,52): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and ((S1 and int) or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(100, 52), + // (101,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and ((S1 and int) or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(101, 34), + // (101,43): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'string'. + // _ = u is {} and ((S1 and int) or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S0", "string").WithLocation(101, 43), + // (101,53): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and ((S1 and int) or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(101, 53), + // (102,26): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or (S1 and C2) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S0", "int").WithLocation(102, 26), + // (102,48): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or (S1 and C2) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(102, 48), + // (103,26): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or ((S1 and C2) or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S0", "int").WithLocation(103, 26), + // (103,49): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or ((S1 and C2) or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(103, 49), + // (104,35): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (((S1 and int) or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(104, 35), + // (104,43): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (((S1 and int) or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S0", "string").WithLocation(104, 43), + // (104,54): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (((S1 and int) or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(104, 54), + // (105,33): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (S1 and int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(105, 33), + // (105,40): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (S1 and int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S0", "string").WithLocation(105, 40), + // (105,50): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (S1 and int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(105, 50) + ); + } + + [Fact] + public void PatternWrongType_BinaryPattern_04_Disjunction_Snap_To_Previous_UnionType_02() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S0 +{ + private readonly object _value; + public S0(byte x) { _value = x; } + public S0(S1 x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2; +class C3; +"; + var src2 = @" +class Program +{ + static void Test1(S0? u) + { +#line 100 + _ = u is {} and ((S1 and int) or string or C3); + _ = u is {} and ((S1 and int) or (string or C3)); + _ = u is {} and (int or (S1 and C2) or C3); + _ = u is {} and (int or ((S1 and C2) or C3)); + _ = u is {} and (((S1 and int) or string) or C3); + _ = u is {} and (S1 and int or string or C3); + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and ((S1 and int) or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(100, 34), + // (100,42): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'string'. + // _ = u is {} and ((S1 and int) or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S0", "string").WithLocation(100, 42), + // (100,52): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and ((S1 and int) or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(100, 52), + // (101,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and ((S1 and int) or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(101, 34), + // (101,43): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'string'. + // _ = u is {} and ((S1 and int) or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S0", "string").WithLocation(101, 43), + // (101,53): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and ((S1 and int) or (string or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(101, 53), + // (102,26): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or (S1 and C2) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S0", "int").WithLocation(102, 26), + // (102,48): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or (S1 and C2) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(102, 48), + // (103,26): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (int or ((S1 and C2) or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S0", "int").WithLocation(103, 26), + // (103,49): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (int or ((S1 and C2) or C3)); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(103, 49), + // (104,35): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (((S1 and int) or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(104, 35), + // (104,43): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (((S1 and int) or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S0", "string").WithLocation(104, 43), + // (104,54): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (((S1 and int) or string) or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(104, 54), + // (105,33): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // _ = u is {} and (S1 and int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(105, 33), + // (105,40): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'string'. + // _ = u is {} and (S1 and int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S0", "string").WithLocation(105, 40), + // (105,50): error CS8121: An expression of type 'S0' cannot be handled by a pattern of type 'C3'. + // _ = u is {} and (S1 and int or string or C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S0", "C3").WithLocation(105, 50) + ); + } + + [Fact] + public void PatternWrongType_BinaryPattern_05_Conjunction_Pass_UnionType_Through() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +struct S0 +{ + private readonly object _value; + public S0(byte x) { _value = x; } + public S0(S1 x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2; +class C3; +"; + var src2 = @" +class Program +{ + static void Test1(object u) + { +#line 100 + _ = u is S1 and string; + _ = u is (S1 and object) and C3; + _ = u is S1 and object and C3; + } + + static void Test2(object u) + { +#line 200 + _ = u is S0 and S1 and C3; + _ = u is (S0 and S1) and C3; + _ = u is S0 and (S1 and C3); + } +} +"; + var comp = CreateCompilation([src2, src1, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics( + // (100,25): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'string'. + // _ = u is S1 and string; + Diagnostic(ErrorCode.ERR_PatternWrongType, "string").WithArguments("S1", "string").WithLocation(100, 25), + // (101,38): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is (S1 and object) and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(101, 38), + // (102,36): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is S1 and object and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(102, 36), + // (200,32): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is S0 and S1 and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(200, 32), + // (201,34): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is (S0 and S1) and C3; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(201, 34), + // (202,33): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // _ = u is S0 and (S1 and C3); + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(202, 33) + ); + } + + [Fact] + public void Exhaustiveness_01() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } +#nullable enable + public S1(string x) { _value = x; } + public object? Value => _value; +#nullable disable +} + +class Program +{ + static int Test1(S1 u) + { +#line 100 + return u switch { int => 1, string => 2, null => 3 }; + } + + static int Test2(S1 u) + { +#line 200 + return u switch { int => 1, null => 3, string => 2 }; + } + + static int Test3(S1 u) + { +#line 300 + return u switch { null => 3, int => 1, string => 2 }; + } + + static int Test4(S1 u) + { +#line 400 + return u switch { int => 1, string => 2 }; + } + + static int Test5(S1 u) + { +#nullable enable +#line 500 + return u switch { int => 1, string => 2 }; +#nullable disable + } + + static int Test6(S1 u) + { +#line 600 + return u switch { int => 1, null => 3 }; + } + + static int Test7(S1 u) + { +#line 700 + return u switch { null => 3, int => 1 }; + } + + static int Test8(S1 u) + { +#line 800 + return u switch { int => 1 }; + } + + static int Test9(S1 u) + { +#line 900 + return u switch { not int => 1 }; + } + + static int Test10(S1 u) + { +#line 1000 + return u switch { null => 3, not int => 1 }; + } + + static int Test11(S1 u) + { +#line 1100 + return u switch { not null => 1 }; + } + + static int Test11_5(S1 u) + { +#nullable enable +#line 1150 + return u switch { not null => 1 }; +#nullable disable + } + + static int Test12(S1 u) + { +#line 1200 + return u switch { null => 3, not null => 1 }; + } + + static int Test13(S1 u) + { +#line 1300 + return u switch { not null => 3, null => 1 }; + } + + static int Test14(S1 u) + { +#line 1400 + return u switch { { } => 1, null => 3 }; + } + + static int Test15(S1 u) + { +#line 1500 + return u switch { null => 3, var x => 1 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + var verifier = CompileAndVerify(comp).VerifyDiagnostics( + // (100,50): hidden CS9335: The pattern is redundant. + // return u switch { int => 1, string => 2, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(100, 50), + // (200,48): hidden CS9335: The pattern is redundant. + // return u switch { int => 1, null => 3, string => 2 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "string").WithLocation(200, 48), + // (300,48): hidden CS9335: The pattern is redundant. + // return u switch { null => 3, int => 1, string => 2 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "string").WithLocation(300, 48), + // (500,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // return u switch { int => 1, string => 2 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(500, 18), + // (600,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'string' is not covered. + // return u switch { int => 1, null => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("string").WithLocation(600, 18), + // (700,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'string' is not covered. + // return u switch { null => 3, int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("string").WithLocation(700, 18), + // (800,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'string' is not covered. + // return u switch { int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("string").WithLocation(800, 18), + // (900,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'int' is not covered. + // return u switch { not int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("int").WithLocation(900, 18), + // (1000,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'int' is not covered. + // return u switch { null => 3, not int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("int").WithLocation(1000, 18), + // (1150,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // return u switch { not null => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(1150, 18), + // (1200,42): hidden CS9335: The pattern is redundant. + // return u switch { null => 3, not null => 1 }; + // Note the location, the diagnostic is for 'null' in 'not null' of the second case rather than for 'null' in the first case. + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(1200, 42), + // (1300,42): hidden CS9335: The pattern is redundant. + // return u switch { not null => 3, null => 1 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(1300, 42), + // (1400,37): hidden CS9335: The pattern is redundant. + // return u switch { { } => 1, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(1400, 37) + ); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 48 (0x30) + .maxstack 1 + .locals init (int V_0, + object V_1) + IL_0000: ldarga.s V_0 + IL_0002: call ""object S1.Value.get"" + IL_0007: stloc.1 + IL_0008: ldloc.1 + IL_0009: isinst ""int"" + IL_000e: brtrue.s IL_001d + IL_0010: ldloc.1 + IL_0011: isinst ""string"" + IL_0016: brtrue.s IL_0021 + IL_0018: ldloc.1 + IL_0019: brfalse.s IL_0025 + IL_001b: br.s IL_0029 + IL_001d: ldc.i4.1 + IL_001e: stloc.0 + IL_001f: br.s IL_002e + IL_0021: ldc.i4.2 + IL_0022: stloc.0 + IL_0023: br.s IL_002e + IL_0025: ldc.i4.3 + IL_0026: stloc.0 + IL_0027: br.s IL_002e + IL_0029: call ""void .ThrowInvalidOperationException()"" + IL_002e: ldloc.0 + IL_002f: ret +} +"); + } + + [Fact] + public void Exhaustiveness_02() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int? x) { _value = x; } +#nullable enable + public S1(string x) { _value = x; } + public object? Value => _value; +#nullable disable +} + +class Program +{ + static int Test1(S1 u) + { +#line 100 + return u switch { int => 1, string => 2, null => 3 }; + } + + static int Test2(S1 u) + { +#line 200 + return u switch { int => 1, null => 3, string => 2 }; + } + + static int Test3(S1 u) + { +#line 300 + return u switch { null => 3, int => 1, string => 2 }; + } + + static int Test4(S1 u) + { +#line 400 + return u switch { int => 1, string => 2 }; + } + + static int Test5(S1 u) + { +#nullable enable +#line 500 + return u switch { int => 1, string => 2 }; +#nullable disable + } + + static int Test6(S1 u) + { +#line 600 + return u switch { int => 1, null => 3 }; + } + + static int Test7(S1 u) + { +#line 700 + return u switch { null => 3, int => 1 }; + } + + static int Test8(S1 u) + { +#line 800 + return u switch { int => 1 }; + } + + static int Test9(S1 u) + { +#line 900 + return u switch { not int => 1 }; + } + + static int Test10(S1 u) + { +#line 1000 + return u switch { null => 3, not int => 1 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,50): hidden CS9335: The pattern is redundant. + // return u switch { int => 1, string => 2, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(100, 50), + // (200,48): hidden CS9335: The pattern is redundant. + // return u switch { int => 1, null => 3, string => 2 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "string").WithLocation(200, 48), + // (300,48): hidden CS9335: The pattern is redundant. + // return u switch { null => 3, int => 1, string => 2 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "string").WithLocation(300, 48), + // (500,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // return u switch { int => 1, string => 2 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(500, 18), + // (600,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'string' is not covered. + // return u switch { int => 1, null => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("string").WithLocation(600, 18), + // (700,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'string' is not covered. + // return u switch { null => 3, int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("string").WithLocation(700, 18), + // (800,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'string' is not covered. + // return u switch { int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("string").WithLocation(800, 18), + // (900,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'int' is not covered. + // return u switch { not int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("int").WithLocation(900, 18), + // (1000,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'int' is not covered. + // return u switch { null => 3, not int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("int").WithLocation(1000, 18) + ); + } + + [Fact] + public void Exhaustiveness_03() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } +#nullable enable + public S1(string x) { _value = x; } + public object? Value => _value; +#nullable disable +} + +class Program +{ + static int Test1(S1 u) + { +#line 100 + return u switch { not null => 2, null => 3 }; + } + + static int Test2(S1 u) + { +#nullable enable +#line 200 + return u switch { not null => 2, null => 3 }; +#nullable disable + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,42): hidden CS9335: The pattern is redundant. + // return u switch { not null => 2, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(100, 42), + // (200,42): hidden CS9335: The pattern is redundant. + // return u switch { not null => 2, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(200, 42) + ); + } + + [Fact] + public void Exhaustiveness_04() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(bool x) { _value = x; } +#nullable enable + public S1(string x) { _value = x; } + public object? Value => _value; +#nullable disable +} + +class Program +{ + static int Test1(S1 u) + { +#line 100 + return u switch { true => 1, false => 4, string => 2, null => 3 }; + } + + static int Test2(S1 u) + { +#line 200 + return u switch { true => 1, false => 4, string => 2 }; + } + + static int Test3(S1 u) + { +#nullable enable +#line 300 + return u switch { true => 1, false => 4, string => 2 }; +#nullable disable + } + + static int Test4(S1 u) + { +#line 400 + return u switch { true => 1, string => 2, null => 3 }; + } + + static int Test5(S1 u) + { +#line 500 + return u switch { null => 3 , true => 1, string => 2 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,63): hidden CS9335: The pattern is redundant. + // return u switch { true => 1, false => 4, string => 2, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(100, 63), + // (300,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // return u switch { true => 1, false => 4, string => 2 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(300, 18), + // (400,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'false' is not covered. + // return u switch { true => 1, string => 2, null => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("false").WithLocation(400, 18), + // (500,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'false' is not covered. + // return u switch { null => 3 , true => 1, string => 2 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("false").WithLocation(500, 18) + ); + } + + [Fact] + public void Exhaustiveness_05() + { + var src1 = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object? _value; + + public C1(){} + public C1(int x) { _value = x; } + public C1(string? x) { _value = x; } + public object? Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C1(10))); + System.Console.Write(Test1(new C1())); + System.Console.Write(Test1(new C1(""10""))); + System.Console.Write(Test1(null)); + + System.Console.Write(' '); + System.Console.Write(Test2(new C1(10))); + System.Console.Write(Test2(new C1())); + System.Console.Write(Test2(new C1(""10""))); + System.Console.Write(Test2(null)); + } + + static int Test1(C1? u) + { +#line 26 + return u switch { int => 1, string => 2, null => 3 }; + } + + static int Test2(C1? u) + { +#line 31 + return u switch { int => -1, string => -2, _ => -3 }; + } + + static int Test3(C2? u) + { + return u switch { { Value: int } => -1, { Value: string } => -2, { Value: null } => -3, _ => -4 }; + } + + static int Test4(C1? u) + { + return u switch { _ => 3 }; + } + + static int Test5(C2 u) + { +#line 46 + return u switch { null => -4, { Value: int } => -1, { Value: string } => -2, { Value: object } => -3 }; + } +} + +class C2 +{ + public object? Value => null; +} + +"; + var comp1 = CreateCompilation([src1, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp1, expectedOutput: "1323 -1-3-2-3").VerifyDiagnostics( + // (26,50): hidden CS9335: The pattern is redundant. + // return u switch { int => 1, string => 2, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(26, 50), + // (46,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern '{ Value: null }' is not covered. + // return u switch { null => -4, { Value: int } => -1, { Value: string } => -2, { Value: object } => -3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("{ Value: null }").WithLocation(46, 18) + ); + + var src2 = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object? _value; + + public C1(){} + public C1(int x) { _value = x; } + public C1(string? x) { _value = x; } + public object? Value => _value; +} + +class Program +{ + static int Test2(C1? u) + { +#line 31 + return u switch { int => -1, string => -2, null => -3, _ => -4 }; + } +} + +class C2 +{ + public object? Value => null; +} + +"; + var comp2 = CreateCompilation([src2, UnionAttributeSource]); + comp2.VerifyDiagnostics( + // (31,64): error CS8510: The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match. + // return u switch { int => -1, string => -2, null => -3, _ => -4 }; + Diagnostic(ErrorCode.ERR_SwitchArmSubsumed, "_").WithLocation(31, 64) + ); + } + + [Fact] + public void Exhaustiveness_06() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object? _value; + + public C1(){} + public C1(int x) { _value = x; } + public C1(string? x) { _value = x; } + public object? Value => _value; +} + +class Program +{ + static int Test4(C1? u) + { +#line 41 + return u switch { int => 1, string => 2, not null => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (41,50): error CS8510: The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match. + // return u switch { int => 1, string => 2, not null => 3 }; + Diagnostic(ErrorCode.ERR_SwitchArmSubsumed, "not null").WithLocation(41, 50), + + // The following warning is for 'u.Value' missing null check. + + // (41,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // return u switch { int => 1, string => 2, not null => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(41, 18) + ); + } + + [Fact] + public void Exhaustiveness_07() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C1 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; + +class Program +{ + static int Test1(S1 u) + { +#line 17 + return u switch { int => 1, C2 => 2, null => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (17,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'C1' is not covered. + // return u switch { int => 1, C2 => 2, null => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("C1").WithLocation(17, 18) + ); + } + + [Fact] + public void Exhaustiveness_08() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +class C1; +class C2 : C1; + +class Program +{ + static int Test1(S1 u) + { +#line 17 + return u switch { int => 1, C1 => 2, null => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (17,46): hidden CS9335: The pattern is redundant. + // return u switch { int => 1, C1 => 2, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(17, 46) + ); + } + + [Fact] + public void Exhaustiveness_09() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(C2 x) { _value = x; } + public object Value => _value; +} + +interface I1; +class C2; +class C3; + +class Program +{ + static int Test1(S1 u) + { +#line 18 + return u switch { int => 1, I1 => 2, null => 3 }; + } + + static int Test2(S1 u) + { + return u switch { int => 1, I1 => 2, C2 => 4, null => 3 }; + } + + static int Test3(S1 u) + { + return u switch { int => 1, I1 => 2, C3 => 5, C2 => 4, null => 3 }; + } + + static int Test4(S1 u) + { + return u switch { int => 1, I1 => 2, null => 3, C2 => 4 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (18,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'C2' is not covered. + // return u switch { int => 1, I1 => 2, null => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("C2").WithLocation(18, 18), + // (23,55): hidden CS9335: The pattern is redundant. + // return u switch { int => 1, I1 => 2, C2 => 4, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(23, 55), + // (28,46): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'C3'. + // return u switch { int => 1, I1 => 2, C3 => 5, C2 => 4, null => 3 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "C3").WithArguments("S1", "C3").WithLocation(28, 46), + // (33,57): hidden CS9335: The pattern is redundant. + // return u switch { int => 1, I1 => 2, null => 3, C2 => 4 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "C2").WithLocation(33, 57) + ); + } + + [Fact] + public void Exhaustiveness_10() + { + var src = +@" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(Q x) { _value = x; } + public S1(int x) { _value = x; } + public object Value => _value; +} + +class C +{ +#line 12 + int M2(S1 o) => o switch { not (Q(1, 2.5) { P1: 1 } and Q(3, 4, 5) { P2: 2 }) => 1 }; + int M3(S1 o) => o switch { null => 0, not (Q(1, 2.5) { P1: 1 } and Q(3, 4, 5) { P2: 2 }) => 1 }; +} +class Q +{ + public void Deconstruct(out object o1, out object o2) => throw null!; + public void Deconstruct(out object o1, out object o2, out object o3) => throw null!; + public int P1 = 5; + public int P2 = 6; +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (12,23): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Q(1, 2.5D) and (3, 4, 5) { P1: 1, P2: 2 }' is not covered. + // int M2(S1 o) => o switch { not (Q(1, 2.5) { P1: 1 } and Q(3, 4, 5) { P2: 2 }) => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("Q(1, 2.5D) and (3, 4, 5) { P1: 1, P2: 2 }").WithLocation(12, 23), + // (13,23): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Q(1, 2.5D) and (3, 4, 5) { P1: 1, P2: 2 }' is not covered. + // int M3(S1 o) => o switch { null => 0, not (Q(1, 2.5) { P1: 1 } and Q(3, 4, 5) { P2: 2 }) => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("Q(1, 2.5D) and (3, 4, 5) { P1: 1, P2: 2 }").WithLocation(13, 23) + ); + } + + [Fact] + public void Exhaustiveness_11() + { + var src = +@" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(Q x) { _value = x; } + public S1(int x) { _value = x; } + public object Value => _value; +} + +class C +{ + int M2(S1 o) +#line 100 + => o switch { int => 1, Q { P1: true } => 2 }; + + int M3(S1 o) +#line 200 + => o switch { Q { P1: true } => 2, int => 1 }; + + int M4(S1 o) +#line 300 + => o switch { null => 0, int => 1, Q { P1: true } => 2 }; + + int M5(S1 o) +#line 400 + => o switch { null => 0, Q { P1: true } => 2, int => 1 }; +} +class Q +{ + public bool P1 = false; +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,14): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Q{ P1: false }' is not covered. + // => o switch { int => 1, Q { P1: true } => 2 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("Q{ P1: false }").WithLocation(100, 14), + // (200,14): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Q{ P1: false }' is not covered. + // => o switch { Q { P1: true } => 2, int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("Q{ P1: false }").WithLocation(200, 14), + // (300,14): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Q{ P1: false }' is not covered. + // => o switch { null => 0, int => 1, Q { P1: true } => 2 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("Q{ P1: false }").WithLocation(300, 14), + // (400,14): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'Q{ P1: false }' is not covered. + // => o switch { null => 0, Q { P1: true } => 2, int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("Q{ P1: false }").WithLocation(400, 14) + ); + } + + [Fact] + public void Exhaustiveness_12() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } +#nullable enable + public S1(string x) { _value = x; } + public object? Value => _value; +#nullable disable +} + +class Program +{ + static int Test1(S1 u) + { +#line 100 + return u switch { object => 1, null => 3 }; + } + + static int Test3(S1 u) + { +#line 300 + return u switch { null => 3, object => 2 }; + } + + static int Test4(S1 u) + { +#line 400 + return u switch { object => 2 }; + } + + static int Test5(S1 u) + { +#nullable enable +#line 500 + return u switch { object => 2 }; +#nullable disable + } + + static int Test9(S1 u) + { +#line 900 + return u switch { not object => 1 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,40): hidden CS9335: The pattern is redundant. + // return u switch { object => 1, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(100, 40), + // (300,38): hidden CS9335: The pattern is redundant. + // return u switch { null => 3, object => 2 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "object").WithLocation(300, 38), + // (500,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // return u switch { object => 2 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(500, 18), + // (900,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'not null' is not covered. + // return u switch { not object => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("not null").WithLocation(900, 18) + ); + } + + [Fact] + public void Exhaustiveness_13() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; +#nullable enable + public S1(string x) { _value = x; } + public object? Value => _value; +#nullable disable +} + +class Program +{ + static int Test1(S1 u) + { +#line 100 + return u switch { string => 2, null => 3 }; + } + + static int Test4(S1 u) + { +#line 400 + return u switch { string => 2 }; + } + + static int Test5(S1 u) + { +#nullable enable +#line 500 + return u switch { string => 2 }; +#nullable disable + } + + static int Test6(S1 u) + { +#line 600 + return u switch { null => 3, string => 2 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + var verifier = CompileAndVerify(comp).VerifyDiagnostics( + // (100,40): hidden CS9335: The pattern is redundant. + // return u switch { string => 2, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(100, 40), + // (500,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // return u switch { string => 2 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(500, 18), + // (600,38): hidden CS9335: The pattern is redundant. + // return u switch { null => 3, string => 2 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "string").WithLocation(600, 38) + ); + } + + [Fact] + public void Exhaustiveness_14() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } +#nullable enable + public object? Value => _value; +#nullable disable +} + +class Program +{ + static int Test1(S1 u) + { +#line 100 + return u switch { int => 1, null => 3 }; + } + + static int Test4(S1 u) + { +#line 400 + return u switch { int => 1 }; + } + + static int Test5(S1 u) + { +#nullable enable +#line 500 + return u switch { int => 1 }; +#nullable disable + } + + static int Test6(S1 u) + { +#line 600 + return u switch { null => 3, int => 1 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + var verifier = CompileAndVerify(comp).VerifyDiagnostics( + // (100,37): hidden CS9335: The pattern is redundant. + // return u switch { int => 1, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(100, 37), + // (500,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // return u switch { int => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(500, 18), + // (600,38): hidden CS9335: The pattern is redundant. + // return u switch { null => 3, int => 1 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "int").WithLocation(600, 38) + ); + } + + [Fact] + public void EmptyUnion_01() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public object Value => null!; +} + +class Program +{ + static int Test1(S1 u) + { +#line 100 + return u switch { int => 1, null => 3 }; + } + + static int Test2(S1 u) + { +#line 200 + return u switch { null => 1 }; + } + + static int Test3(S1 u) + { +#line 300 + return u switch { null => 3, object => 1 }; + } + + static int Test4(S1 u) + { +#line 400 + return u switch { int => 1 }; + } + + static int Test5(S1 u) + { +#line 500 + return u switch { object => 1 }; + } + + static int Test6(S1 u) + { +#line 600 + return u switch { not object => 1 }; + } + + static int Test7(S1 u) + { +#line 700 + return u switch { null => 3, not object => 1 }; + } + + static int Test8(S1 u) + { +#line 800 + return u switch { object => 1, null => 3 }; + } + + static int Test9(S1 u) + { +#line 900 + return u switch { not null => 1 }; + } + + static int Test10(S1 u) + { +#line 1000 + return u switch { null => 3, not null => 1 }; + } + + static int Test11(S1 u) + { +#line 1100 + return u switch { not null => 3, null => 1 }; + } + + static int Test12(S1 u) + { +#line 1200 + return u switch { { } => 1, null => 3 }; + } + + static int Test13(S1 u) + { +#line 1300 + return u switch { null => 3, var x => 1 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // return u switch { int => 1, null => 3 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(100, 27), + // (200,18): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern 'not null' is not covered. + // return u switch { null => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustive, "switch").WithArguments("not null").WithLocation(200, 18), + // (300,38): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'object'. + // return u switch { null => 3, object => 1 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "object").WithArguments("S1", "object").WithLocation(300, 38), + // (400,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'int'. + // return u switch { int => 1 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "int").WithArguments("S1", "int").WithLocation(400, 27), + // (500,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'object'. + // return u switch { object => 1 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "object").WithArguments("S1", "object").WithLocation(500, 27), + // (600,31): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'object'. + // return u switch { not object => 1 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "object").WithArguments("S1", "object").WithLocation(600, 31), + // (700,43): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'object'. + // return u switch { null => 3, not object => 1 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "object").WithArguments("S1", "object").WithLocation(700, 43), + // (800,27): error CS8121: An expression of type 'S1' cannot be handled by a pattern of type 'object'. + // return u switch { object => 1, null => 3 }; + Diagnostic(ErrorCode.ERR_PatternWrongType, "object").WithArguments("S1", "object").WithLocation(800, 27), + // (900,18): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // return u switch { not null => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(900, 18), + // (1000,42): hidden CS9335: The pattern is redundant. + // return u switch { null => 3, not null => 1 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(1000, 42), + // (1100,42): hidden CS9335: The pattern is redundant. + // return u switch { not null => 3, null => 1 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(1100, 42), + // (1200,37): hidden CS9335: The pattern is redundant. + // return u switch { { } => 1, null => 3 }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(1200, 37) + ); + } + + [Fact] + public void UnionConversion_01_Implicit() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + Test2(); + Test3(); + Test4(); + Test5(); + } + + static S1 Test1() + { + System.Console.Write(""1-""); + /**/ return 10; /**/ + } + + static S1 Test2() + { + System.Console.Write(""2-""); + return default; + } + + static S1 Test3() + { + System.Console.Write(""3-""); + return default(S1); + } + + static S1 Test4() + { + System.Console.Write(""4-""); + return null; + } + + static S1 Test5() + { + System.Console.Write(""5-""); + return ""11""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + var ten = GetSyntax(tree, "10"); + + var symbolInfo = model.GetSymbolInfo(ten); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + Assert.Null(symbolInfo.Symbol); + Assert.Empty(symbolInfo.CandidateSymbols); + + var typeInfo = model.GetTypeInfo(ten); + Assert.Equal("System.Int32", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("S1", typeInfo.ConvertedType.ToTestDisplayString()); + + Conversion conversion = model.GetConversion(ten); + Assert.True(conversion.Exists); + Assert.True(conversion.IsValid); + Assert.True(conversion.IsImplicit); + Assert.False(conversion.IsExplicit); + Assert.Equal(ConversionKind.Union, conversion.Kind); + Assert.Equal(LookupResultKind.Viable, conversion.ResultKind); + Assert.True(conversion.IsUnion); + Assert.False(conversion.IsUserDefined); + AssertEx.Equal("S1..ctor(System.Int32 x)", conversion.Method.ToTestDisplayString()); + AssertEx.Equal("S1..ctor(System.Int32 x)", conversion.MethodSymbol.ToTestDisplayString()); + Assert.Null(conversion.BestUserDefinedConversionAnalysis); + Assert.Equal(Conversion.NoConversion, conversion.UserDefinedFromConversion); + Assert.Equal(Conversion.NoConversion, conversion.UserDefinedToConversion); + Assert.NotNull(conversion.BestUnionConversionAnalysis); + Assert.Empty(conversion.OriginalUserDefinedConversions); + Assert.True(conversion.UnderlyingConversions.IsDefault); + Assert.False(conversion.IsArrayIndex); + Assert.False(conversion.IsExtensionMethod); + + CommonConversion commonConversion = conversion.ToCommonConversion(); + + Assert.True(commonConversion.Exists); + Assert.True(commonConversion.IsImplicit); + Assert.True(commonConversion.IsUnion); + Assert.False(commonConversion.IsUserDefined); + AssertEx.Equal("S1..ctor(System.Int32 x)", commonConversion.MethodSymbol.ToTestDisplayString()); + + VerifyOperationTreeForTest(comp, """ +IReturnOperation (OperationKind.Return, Type: null) (Syntax: 'return 10;') + ReturnedValue: + IConversionOperation (TryCast: False, Unchecked) (OperatorMethod: S1..ctor(System.Int32 x)) (OperationKind.Conversion, Type: S1, IsImplicit) (Syntax: '10') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False, IsUnion: True) (MethodSymbol: S1..ctor(System.Int32 x)) + Operand: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 10) (Syntax: '10') +"""); + + var verifier = CompileAndVerify(comp, expectedOutput: "1-int {10} 2-3-4-string {} 5-string {11}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldc.i4.s 10 + IL_000c: newobj ""S1..ctor(int)"" + IL_0011: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 20 (0x14) + .maxstack 1 + .locals init (S1 V_0) + IL_0000: ldstr ""2-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1"" + IL_0012: ldloc.0 + IL_0013: ret +} +"); + + verifier.VerifyIL("Program.Test3", @" +{ + // Code size 20 (0x14) + .maxstack 1 + .locals init (S1 V_0) + IL_0000: ldstr ""3-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1"" + IL_0012: ldloc.0 + IL_0013: ret +} +"); + + verifier.VerifyIL("Program.Test4", @" +{ + // Code size 17 (0x11) + .maxstack 1 + IL_0000: ldstr ""4-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: newobj ""S1..ctor(string)"" + IL_0010: ret +} +"); + + verifier.VerifyIL("Program.Test5", @" +{ + // Code size 21 (0x15) + .maxstack 1 + IL_0000: ldstr ""5-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldstr ""11"" + IL_000f: newobj ""S1..ctor(string)"" + IL_0014: ret +} +"); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "1-int {10} 2-3-4-string {} 5-string {11}").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (37,27): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // /**/ return 10; /**/ + Diagnostic(ErrorCode.ERR_FeatureInPreview, "10").WithArguments("unions").WithLocation(37, 27), + // (55,16): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "null").WithArguments("unions").WithLocation(55, 16), + // (61,16): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return "11"; + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"""11""").WithArguments("unions").WithLocation(61, 16) + ); + } + + [Fact] + public void UnionConversion_02_Implicit_Class() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + Test2(); + Test3(); + Test4(); + Test5(); + } + + static S1 Test1() + { + System.Console.Write(""1-""); + return 10; + } + + static S1 Test2() + { + System.Console.Write(""2-""); + return default; + } + + static S1 Test3() + { + System.Console.Write(""3-""); + return default(S1); + } + + static S1 Test4() + { + System.Console.Write(""4-""); + return null; + } + + static S1 Test5() + { + System.Console.Write(""5-""); + return ""11""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "1-int {10} 2-3-4-5-string {11}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldc.i4.s 10 + IL_000c: newobj ""S1..ctor(int)"" + IL_0011: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 12 (0xc) + .maxstack 1 + IL_0000: ldstr ""2-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: ret +} +"); + + verifier.VerifyIL("Program.Test3", @" +{ + // Code size 12 (0xc) + .maxstack 1 + IL_0000: ldstr ""3-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: ret +} +"); + + verifier.VerifyIL("Program.Test4", @" +{ + // Code size 12 (0xc) + .maxstack 1 + IL_0000: ldstr ""4-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: ret +} +"); + + verifier.VerifyIL("Program.Test5", @" +{ + // Code size 21 (0x15) + .maxstack 1 + IL_0000: ldstr ""5-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldstr ""11"" + IL_000f: newobj ""S1..ctor(string)"" + IL_0014: ret +} +"); + } + + [Fact] + public void UnionConversion_03_Cast() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + Test2(); + Test3(); + Test4(); + Test5(); + } + + static S1 Test1() + { + System.Console.Write(""1-""); + return /**/ (S1)10 /**/; + } + + static S1 Test2() + { + System.Console.Write(""2-""); + return (S1)default; + } + + static S1 Test3() + { + System.Console.Write(""3-""); + return (S1)default(S1); + } + + static S1 Test4() + { + System.Console.Write(""4-""); + return (S1)null; + } + + static S1 Test5() + { + System.Console.Write(""5-""); + return (S1)""11""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + var cast = GetSyntax(tree, "(S1)10"); + + var typeInfo = model.GetTypeInfo(cast); + Assert.Equal("S1", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("S1", typeInfo.ConvertedType.ToTestDisplayString()); + + Conversion conversion = model.GetConversion(cast); + Assert.True(conversion.IsIdentity); + + var symbolInfo = model.GetSymbolInfo(cast); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + AssertEx.Equal("S1..ctor(System.Int32 x)", symbolInfo.Symbol.ToTestDisplayString()); + Assert.Empty(symbolInfo.CandidateSymbols); + + VerifyOperationTreeForTest(comp, """ +IConversionOperation (TryCast: False, Unchecked) (OperatorMethod: S1..ctor(System.Int32 x)) (OperationKind.Conversion, Type: S1) (Syntax: '(S1)10') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False, IsUnion: True) (MethodSymbol: S1..ctor(System.Int32 x)) + Operand: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 10) (Syntax: '10') +"""); + + var ten = GetSyntax(tree, "10"); + + typeInfo = model.GetTypeInfo(ten); + Assert.Equal("System.Int32", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("System.Int32", typeInfo.ConvertedType.ToTestDisplayString()); + + conversion = model.GetConversion(ten); + Assert.True(conversion.IsIdentity); + + var verifier = CompileAndVerify(comp, expectedOutput: "1-int {10} 2-3-4-string {} 5-string {11}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldc.i4.s 10 + IL_000c: newobj ""S1..ctor(int)"" + IL_0011: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 20 (0x14) + .maxstack 1 + .locals init (S1 V_0) + IL_0000: ldstr ""2-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1"" + IL_0012: ldloc.0 + IL_0013: ret +} +"); + + verifier.VerifyIL("Program.Test3", @" +{ + // Code size 20 (0x14) + .maxstack 1 + .locals init (S1 V_0) + IL_0000: ldstr ""3-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1"" + IL_0012: ldloc.0 + IL_0013: ret +} +"); + + verifier.VerifyIL("Program.Test4", @" +{ + // Code size 17 (0x11) + .maxstack 1 + IL_0000: ldstr ""4-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: newobj ""S1..ctor(string)"" + IL_0010: ret +} +"); + + verifier.VerifyIL("Program.Test5", @" +{ + // Code size 21 (0x15) + .maxstack 1 + IL_0000: ldstr ""5-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldstr ""11"" + IL_000f: newobj ""S1..ctor(string)"" + IL_0014: ret +} +"); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularNext); + CompileAndVerify(comp, expectedOutput: "1-int {10} 2-3-4-string {} 5-string {11}").VerifyDiagnostics(); + + comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe, parseOptions: TestOptions.Regular14); + comp.VerifyDiagnostics( + // (37,27): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return /**/ (S1)10 /**/; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "(S1)10").WithArguments("unions").WithLocation(37, 27), + // (55,16): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return (S1)null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "(S1)null").WithArguments("unions").WithLocation(55, 16), + // (61,16): error CS8652: The feature 'unions' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // return (S1)"11"; + Diagnostic(ErrorCode.ERR_FeatureInPreview, @"(S1)""11""").WithArguments("unions").WithLocation(61, 16) + ); + } + + [Fact] + public void UnionConversion_04_Cast_Class() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + Test2(); + Test3(); + Test4(); + Test5(); + } + + static S1 Test1() + { + System.Console.Write(""1-""); + return (S1)10; + } + + static S1 Test2() + { + System.Console.Write(""2-""); + return (S1)default; + } + + static S1 Test3() + { + System.Console.Write(""3-""); + return (S1)default(S1); + } + + static S1 Test4() + { + System.Console.Write(""4-""); + return (S1)null; + } + + static S1 Test5() + { + System.Console.Write(""5-""); + return (S1)""11""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "1-int {10} 2-3-4-5-string {11}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldc.i4.s 10 + IL_000c: newobj ""S1..ctor(int)"" + IL_0011: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 12 (0xc) + .maxstack 1 + IL_0000: ldstr ""2-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: ret +} +"); + + verifier.VerifyIL("Program.Test3", @" +{ + // Code size 12 (0xc) + .maxstack 1 + IL_0000: ldstr ""3-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: ret +} +"); + + verifier.VerifyIL("Program.Test4", @" +{ + // Code size 12 (0xc) + .maxstack 1 + IL_0000: ldstr ""4-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: ret +} +"); + + verifier.VerifyIL("Program.Test5", @" +{ + // Code size 21 (0x15) + .maxstack 1 + IL_0000: ldstr ""5-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldstr ""11"" + IL_000f: newobj ""S1..ctor(string)"" + IL_0014: ret +} +"); + } + + [Fact] + public void UnionConversion_05_No_Lifted_Form() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) + { + _value = x; + } + public S1(string x) + { + _value = x; + } + public object Value => _value; +} + +class Program +{ + static S1 Test1(int? x) + { +#line 20 + return x; + } + + static S1? Test2(int? y) + { + return y; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + + comp.VerifyDiagnostics( + // (20,16): error CS0029: Cannot implicitly convert type 'int?' to 'S1' + // return x; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "x").WithArguments("int?", "S1").WithLocation(20, 16), + // (25,16): error CS0029: Cannot implicitly convert type 'int?' to 'S1?' + // return y; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "y").WithArguments("int?", "S1?").WithLocation(25, 16) + ); + } + + [Fact] + public void UnionConversion_06_No_Lifted_Form_Class() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) + { + _value = x; + } + public S1(string x) + { + _value = x; + } + public object Value => _value; +} + +class Program +{ + static S1 Test1(int? x) + { +#line 20 + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (20,16): error CS0029: Cannot implicitly convert type 'int?' to 'S1' + // return x; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "x").WithArguments("int?", "S1").WithLocation(20, 16) + ); + } + + [Fact] + public void UnionConversion_07_Ambiguity_First_Declared_Wins() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +public struct S1 +{ + private readonly object _value; + public S1(C1 x) + { + System.Console.Write(""C1""); + _value = x; + } + public S1(C2 x) => throw null; + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +public struct S2 +{ + private readonly object _value; + public S2(C2 x) + { + System.Console.Write(""C2""); + _value = x; + } + public S2(C1 x) => throw null; + public object Value => _value; +} + +public class C1 { } +public class C2 { } +"; + var src2 = @" +class Program +{ + static void Main() + { + Test1(); + Test2(); + } + + static S1 Test1() + { + return null; + } + + static S2 Test2() + { + return null; + } +} +"; + var comp = CreateCompilation([src1, src2, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "C1C2").VerifyDiagnostics(); + + comp = CreateCompilation(src2, references: [comp.EmitToImageReference()], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "C1C2").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_08_Standard_Conversion_For_Source_Allowed() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) => throw null; + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(15); + Test2(16); + } + + static S1 Test1(byte x1) + { + return x1; + } + + static S1 Test2(byte x2) + { + return (S1)x2; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "int {15} int {16}").VerifyDiagnostics(); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + var x1 = GetSyntax(tree, "x1"); + + var symbolInfo = model.GetSymbolInfo(x1); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + AssertEx.Equal("System.Byte x1", symbolInfo.Symbol.ToTestDisplayString()); + Assert.Empty(symbolInfo.CandidateSymbols); + + var typeInfo = model.GetTypeInfo(x1); + Assert.Equal("System.Byte", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("S1", typeInfo.ConvertedType.ToTestDisplayString()); + + Conversion conversion = model.GetConversion(x1); + Assert.True(conversion.IsUnion); + Assert.False(conversion.IsUserDefined); + AssertEx.Equal("S1..ctor(System.Int32 x)", conversion.Method.ToTestDisplayString()); + Assert.Null(conversion.BestUserDefinedConversionAnalysis); + Assert.Equal(Conversion.NoConversion, conversion.UserDefinedFromConversion); + Assert.Equal(Conversion.NoConversion, conversion.UserDefinedToConversion); + Assert.NotNull(conversion.BestUnionConversionAnalysis); + Assert.Empty(conversion.OriginalUserDefinedConversions); + Assert.True(conversion.UnderlyingConversions.IsDefault); + + CommonConversion commonConversion = conversion.ToCommonConversion(); + + Assert.True(commonConversion.Exists); + Assert.True(commonConversion.IsImplicit); + Assert.True(commonConversion.IsUnion); + Assert.False(commonConversion.IsUserDefined); + AssertEx.Equal("S1..ctor(System.Int32 x)", commonConversion.MethodSymbol.ToTestDisplayString()); + + VerifyOperationTreeForNode(comp, model, GetSyntax(tree, "return x1;"), """ +IReturnOperation (OperationKind.Return, Type: null) (Syntax: 'return x1;') + ReturnedValue: + IConversionOperation (TryCast: False, Unchecked) (OperatorMethod: S1..ctor(System.Int32 x)) (OperationKind.Conversion, Type: S1, IsImplicit) (Syntax: 'x1') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False, IsUnion: True) (MethodSymbol: S1..ctor(System.Int32 x)) + Operand: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Int32, IsImplicit) (Syntax: 'x1') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: True, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + IParameterReferenceOperation: x1 (OperationKind.ParameterReference, Type: System.Byte) (Syntax: 'x1') +"""); + + var x2 = GetSyntax(tree, "x2"); + + symbolInfo = model.GetSymbolInfo(x2); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + AssertEx.Equal("System.Byte x2", symbolInfo.Symbol.ToTestDisplayString()); + Assert.Empty(symbolInfo.CandidateSymbols); + + typeInfo = model.GetTypeInfo(x2); + Assert.Equal("System.Byte", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("System.Byte", typeInfo.ConvertedType.ToTestDisplayString()); + + conversion = model.GetConversion(x2); + Assert.True(conversion.IsIdentity); + Assert.False(conversion.IsUnion); + Assert.False(conversion.IsUserDefined); + + var cast = GetSyntax(tree, "(S1)x2"); + + symbolInfo = model.GetSymbolInfo(cast); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + AssertEx.Equal("S1..ctor(System.Int32 x)", symbolInfo.Symbol.ToTestDisplayString()); + Assert.Empty(symbolInfo.CandidateSymbols); + + typeInfo = model.GetTypeInfo(cast); + Assert.Equal("S1", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("S1", typeInfo.ConvertedType.ToTestDisplayString()); + + VerifyOperationTreeForNode(comp, model, GetSyntax(tree, "return (S1)x2;"), """ +IReturnOperation (OperationKind.Return, Type: null) (Syntax: 'return (S1)x2;') + ReturnedValue: + IConversionOperation (TryCast: False, Unchecked) (OperatorMethod: S1..ctor(System.Int32 x)) (OperationKind.Conversion, Type: S1) (Syntax: '(S1)x2') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False, IsUnion: True) (MethodSymbol: S1..ctor(System.Int32 x)) + Operand: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Int32, IsImplicit) (Syntax: '(S1)x2') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: True, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + IParameterReferenceOperation: x2 (OperationKind.ParameterReference, Type: System.Byte) (Syntax: 'x2') +"""); + } + + [Fact] + public void UnionConversion_09_NonStandard_Conversion_For_Source_Not_Allowed() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(C1 x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +class C1 +{ + public static implicit operator C1(byte x) => new C1(); +} + +class Program +{ + static S1 Test1(byte x) + { +#line 100 + return x; + } + + static S1 Test2(byte x) + { +#line 200 + return (S1)x; + } + + static C1 Test3(byte x) + { + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,16): error CS0029: Cannot implicitly convert type 'byte' to 'S1' + // return x; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "x").WithArguments("byte", "S1").WithLocation(100, 16), + // (200,16): error CS0030: Cannot convert type 'byte' to 'S1' + // return (S1)x; + Diagnostic(ErrorCode.ERR_NoExplicitConv, "(S1)x").WithArguments("byte", "S1").WithLocation(200, 16) + ); + } + + [Fact] + public void UnionConversion_10_Explicit_Conversion_For_Source_Not_Allowed() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static S1 Test1(long x) + { +#line 100 + return x; + } + static S1 Test2(long x) + { +#line 200 + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,16): error CS0029: Cannot implicitly convert type 'long' to 'S1' + // return x; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "x").WithArguments("long", "S1").WithLocation(100, 16), + // (200,16): error CS0030: Cannot convert type 'long' to 'S1' + // return (S1)x; + Diagnostic(ErrorCode.ERR_NoExplicitConv, "(S1)x").WithArguments("long", "S1").WithLocation(200, 16) + ); + } + + [Fact] + public void UnionConversion_11_Not_Standard_Conversion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(S2 x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null; + public S2(string x) => throw null; + public object Value => throw null; +} + +class C1 +{ + public static implicit operator C1(S2 x) => new C1(); +} + +class Program +{ + static S1 Test1(int x) + { +#line 100 + return x; + } + + static C1 Test2(int x) + { +#line 200 + return x; + } + + static S1 Test3(int x) + { +#line 300 + return (S2)x; + } + + static C1 Test4(int x) + { +#line 400 + return (S2)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,16): error CS0029: Cannot implicitly convert type 'int' to 'S1' + // return x; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "x").WithArguments("int", "S1").WithLocation(100, 16), + // (200,16): error CS0029: Cannot implicitly convert type 'int' to 'C1' + // return x; + Diagnostic(ErrorCode.ERR_NoImplicitConv, "x").WithArguments("int", "C1").WithLocation(200, 16) + ); + } + + [Fact] + public void UnionConversion_12_Implicit_UserDefined_Conversion_Wins() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null; + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public object Value => throw null; + + public static implicit operator S1(int x) + { + System.Console.Write(""implicit operator ""); + return new S1(x.ToString()); + } +} + +class Program +{ + static void Main() + { + Test1(); + Test2(); + } + + static S1 Test1() + { + return 10; + } + + static S1 Test2() + { + return (S1)20; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "implicit operator string {10} implicit operator string {20}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_13_Cast_Explicit_UserDefined_Conversion_Wins() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null; + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public object Value => throw null; + + public static explicit operator S1(int x) + { + System.Console.Write(""explicit operator ""); + return new S1(x.ToString()); + } +} + +class Program +{ + static void Main() + { + Test2(); + } + + static S1 Test2() + { + return (S1)20; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "explicit operator string {20}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_14_Explicit_UserDefined_Conversion_Loses() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public S1(string x) => throw null; + public object Value => throw null; + + public static explicit operator S1(int x) => throw null; +} + +class Program +{ + static void Main() + { + Test1(); + } + + static S1 Test1() + { + return 10; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "int {10}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_15_Cast_From_Base_Class_Not_Union_Conversion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(System.ValueType x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1())); + } + + static S1 Test2(System.ValueType x) + { + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "S1").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: unbox.any ""S1"" + IL_0006: ret +} +"); + } + + [Fact] + public void UnionConversion_16_Implicit_From_Base_Class_Union_Conversion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(System.ValueType x) + { + System.Console.Write(""System.ValueType ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1())); + } + + static S1 Test2(System.ValueType x) + { + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "System.ValueType S1").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: newobj ""S1..ctor(System.ValueType)"" + IL_0006: ret +} +"); + var src2 = @" +struct S1 +{ + public static implicit operator S1(System.ValueType x) + => throw null; +} +struct S2 +{ + public static explicit operator S2(System.ValueType x) + => throw null; +} +"; + CreateCompilation(src2).VerifyDiagnostics( + // (4,37): error CS0553: 'S1.implicit operator S1(ValueType)': user-defined conversions to or from a base type are not allowed + // public static implicit operator S1(System.ValueType x) + Diagnostic(ErrorCode.ERR_ConversionWithBase, "S1").WithArguments("S1.implicit operator S1(System.ValueType)").WithLocation(4, 37), + // (9,37): error CS0553: 'S2.explicit operator S2(ValueType)': user-defined conversions to or from a base type are not allowed + // public static explicit operator S2(System.ValueType x) + Diagnostic(ErrorCode.ERR_ConversionWithBase, "S2").WithArguments("S2.explicit operator S2(System.ValueType)").WithLocation(9, 37) + ); + } + + [Fact] + public void UnionConversion_17_Cast_From_Implemented_Interface_Not_Union_Conversion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 : I1 +{ + public S1(I1 x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +interface I1 { } + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1())); + } + + static S1 Test2(I1 x) + { + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "S1").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: unbox.any ""S1"" + IL_0006: ret +} +"); + } + + [Fact] + public void UnionConversion_18_Implicit_From_Implemented_Interface_Union_Conversion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 : I1 +{ + public S1(I1 x) + { + System.Console.Write(""I1 ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +interface I1 { } + +class Program +{ + static void Main() + { + System.Console.Write(Test2(new S1())); + } + + static S1 Test2(I1 x) + { + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "I1 S1").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: newobj ""S1..ctor(I1)"" + IL_0006: ret +} +"); + var src2 = @" +interface I1 { } + +struct S1 : I1 +{ + public static implicit operator S1(I1 x) + => throw null; +} +struct S2 : I1 +{ + public static explicit operator S2(I1 x) + => throw null; +} +"; + CreateCompilation(src2).VerifyDiagnostics( + // (6,37): error CS0552: 'S1.implicit operator S1(I1)': user-defined conversions to or from an interface are not allowed + // public static implicit operator S1(I1 x) + Diagnostic(ErrorCode.ERR_ConversionWithInterface, "S1").WithArguments("S1.implicit operator S1(I1)").WithLocation(6, 37), + // (11,37): error CS0552: 'S2.explicit operator S2(I1)': user-defined conversions to or from an interface are not allowed + // public static explicit operator S2(I1 x) + Diagnostic(ErrorCode.ERR_ConversionWithInterface, "S2").WithArguments("S2.explicit operator S2(I1)").WithLocation(11, 37) + ); + } + + [Fact] + public void UnionConversion_19_From_Not_Implemented_Interface_Union_Conversion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(I1 x) + { + System.Console.Write(""I1 ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +interface I1 { } + +struct S2 : I1; + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S2())); + System.Console.Write(' '); + System.Console.Write(Test2(new S2())); + } + + static S1 Test1(I1 x) + { + return x; + } + + static S1 Test2(I1 x) + { + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "I1 S1 I1 S1").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: newobj ""S1..ctor(I1)"" + IL_0006: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: newobj ""S1..ctor(I1)"" + IL_0006: ret +} +"); + var src2 = @" +interface I1 { } + +struct S1 +{ + public static implicit operator S1(I1 x) + => throw null; +} +struct S2 +{ + public static explicit operator S2(I1 x) + => throw null; +} +"; + CreateCompilation(src2).VerifyDiagnostics( + // (6,37): error CS0552: 'S1.implicit operator S1(I1)': user-defined conversions to or from an interface are not allowed + // public static implicit operator S1(I1 x) + Diagnostic(ErrorCode.ERR_ConversionWithInterface, "S1").WithArguments("S1.implicit operator S1(I1)").WithLocation(6, 37), + // (11,37): error CS0552: 'S2.explicit operator S2(I1)': user-defined conversions to or from an interface are not allowed + // public static explicit operator S2(I1 x) + Diagnostic(ErrorCode.ERR_ConversionWithInterface, "S2").WithArguments("S2.explicit operator S2(I1)").WithLocation(11, 37) + ); + } + + [Fact] + public void UnionConversion_20_From_Not_Implemented_Interface_Union_Conversion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + public S1(I1 x) + { + System.Console.Write(""I1 ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +interface I1 { } + +struct S2 : I1; + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S2())); + } + + static S1 Test1(I1 x) + { + return x; + } + + static S1 Test2(I1 x) + { + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "I1 S1").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: newobj ""S1..ctor(I1)"" + IL_0006: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 7 (0x7) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: castclass ""S1"" + IL_0006: ret +} +"); + var src2 = @" +interface I1 { } + +class S1 +{ + public static implicit operator S1(I1 x) + => throw null; +} +class S2 +{ + public static explicit operator S2(I1 x) + => throw null; +} +"; + CreateCompilation(src2).VerifyDiagnostics( + // (6,37): error CS0552: 'S1.implicit operator S1(I1)': user-defined conversions to or from an interface are not allowed + // public static implicit operator S1(I1 x) + Diagnostic(ErrorCode.ERR_ConversionWithInterface, "S1").WithArguments("S1.implicit operator S1(I1)").WithLocation(6, 37), + // (11,37): error CS0552: 'S2.explicit operator S2(I1)': user-defined conversions to or from an interface are not allowed + // public static explicit operator S2(I1 x) + Diagnostic(ErrorCode.ERR_ConversionWithInterface, "S2").WithArguments("S2.explicit operator S2(I1)").WithLocation(11, 37) + ); + } + + [Fact] + public void UnionConversion_21_Through_Base_Class_Or_Interface() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(System.ValueType x) + { + System.Console.Write(""System.ValueType {""); + System.Console.Write(x.GetType()); + System.Console.Write(' '); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(System.IComparable x) + { + System.Console.Write(""System.IComparable {""); + System.Console.Write(x.GetType()); + System.Console.Write(' '); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + Test5(); + } + + static S1 Test1() + { + System.Console.Write(""1-""); + return 10; + } + + static S1 Test5() + { + System.Console.Write(""5-""); + return ""11""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "1-System.ValueType {System.Int32 10} 5-System.IComparable {System.String 11}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 23 (0x17) + .maxstack 1 + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldc.i4.s 10 + IL_000c: box ""int"" + IL_0011: newobj ""S1..ctor(System.ValueType)"" + IL_0016: ret +} +"); + + verifier.VerifyIL("Program.Test5", @" +{ + // Code size 21 (0x15) + .maxstack 1 + IL_0000: ldstr ""5-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldstr ""11"" + IL_000f: newobj ""S1..ctor(System.IComparable)"" + IL_0014: ret +} +"); + } + + [Fact] + public void UnionConversion_22_ExpressionTree() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static System.Linq.Expressions.Expression> Test1(int x) + { +#line 13 + return () => x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (13,22): error CS9369: An expression tree may not contain a union conversion. + // return () => x; + Diagnostic(ErrorCode.ERR_ExpressionTreeContainsUnionConversion, "x").WithLocation(13, 22) + ); + } + + [Fact] + public void UnionConversion_23_ClassifyImplicitConversionFromType() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static void Main() + { + Test1(10); + } + + static S1 Test1(int? x) + { + return x ?? new S1(""""); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "int {10}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_24_ClassifyConversionFromType() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static void Main() + { + var x = new S1(); + var y = (0, 123); + (var z, x) = y; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "int {123}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_25_ClassifyConversionFromTypeForCast_Implicit_UserDefined_Conversion_Wins() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null; + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public object Value => throw null; + + public static implicit operator S1(int x) + { + System.Console.Write(""implicit operator ""); + return new S1(x.ToString()); + } +} + +class Program +{ + static void Main() + { + Test1(); + } + + static void Test1() + { + foreach (S1 y in new int[] { 10 }) + { + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "implicit operator string {10}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_26_ClassifyConversionFromTypeForCast_Explicit_UserDefined_Conversion_Wins() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null; + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public object Value => throw null; + + public static explicit operator S1(int x) + { + System.Console.Write(""explicit operator ""); + return new S1(x.ToString()); + } +} + +class Program +{ + static void Main() + { + Test2(); + } + + static void Test2() + { + foreach (S1 y in new int[] { 20 }) + { + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "explicit operator string {20}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_27_ClassifyConversionFromTypeForCast() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static void Main() + { + Test1(); + } + + static void Test1() + { + foreach (S1 y in new int[] { 10 }) + { + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "int {10}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_28_Under_Tuple_Conversion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(double x) + { + System.Console.Write(""double {""); + System.Console.Write((int)x); + System.Console.Write(""} ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static void Main() + { + Test1((0, 10)); + } + + static (int, S1) Test1((int, byte) x) + { + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "double {10}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 26 (0x1a) + .maxstack 2 + .locals init (System.ValueTuple V_0) + IL_0000: ldarg.0 + IL_0001: stloc.0 + IL_0002: ldloc.0 + IL_0003: ldfld ""int System.ValueTuple.Item1"" + IL_0008: ldloc.0 + IL_0009: ldfld ""byte System.ValueTuple.Item2"" + IL_000e: conv.r8 + IL_000f: newobj ""S1..ctor(double)"" + IL_0014: newobj ""System.ValueTuple..ctor(int, S1)"" + IL_0019: ret +} +"); + } + + [Fact] + public void UnionConversion_30_In_Parameter() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(in int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static void Main() + { + Test1(); + Test2(11); + Test3(12); + } + + static S1 Test1() + { + System.Console.Write(""1-""); + return 10; + } + + static S1 Test2(int x) + { + System.Console.Write(""2-""); + return x; + } + + static S1 Test3(byte x) + { + System.Console.Write(""3-""); + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "1-int {10} 2-int {11} 3-int {12}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 21 (0x15) + .maxstack 1 + .locals init (int V_0) + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldc.i4.s 10 + IL_000c: stloc.0 + IL_000d: ldloca.s V_0 + IL_000f: newobj ""S1..ctor(in int)"" + IL_0014: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldstr ""2-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldarga.s V_0 + IL_000c: newobj ""S1..ctor(in int)"" + IL_0011: ret +} +"); + + verifier.VerifyIL("Program.Test3", @" +{ + // Code size 20 (0x14) + .maxstack 1 + .locals init (int V_0) + IL_0000: ldstr ""3-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldarg.0 + IL_000b: stloc.0 + IL_000c: ldloca.s V_0 + IL_000e: newobj ""S1..ctor(in int)"" + IL_0013: ret +} +"); + } + + [Fact] + public void UnionConversion_31_Ambiguity_In_Vs_Val_First_Declared_Wins() + { + var src1 = @" +[System.Runtime.CompilerServices.Union] +public struct S1 +{ + public S1(in int x) + { + System.Console.Write(""In""); + } + public S1(int x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +[System.Runtime.CompilerServices.Union] +public struct S2 +{ + public S2(int x) + { + System.Console.Write(""Val""); + } + public S2(in int x) => throw null; + public S2(string x) => throw null; + public object Value => throw null; +} +"; + var src2 = @" +class Program +{ + static void Main() + { + Test1(); + Test2(); + } + + static S1 Test1() + { + return 10; + } + + static S2 Test2() + { + return 10; + } +} +"; + var comp = CreateCompilation([src1, src2, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "InVal").VerifyDiagnostics(); + + comp = CreateCompilation(src2, references: [comp.EmitToImageReference()], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "InVal").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_32_No_Params_Expansion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(params int[] x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static S1 Test1(int x) + { +#line 13 + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (13,16): error CS0030: Cannot convert type 'int' to 'S1' + // return (S1)x; + Diagnostic(ErrorCode.ERR_NoExplicitConv, "(S1)x").WithArguments("int", "S1").WithLocation(13, 16) + ); + } + + [Fact] + public void UnionConversion_33_No_Optional() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(byte x) => throw null; + public S1(string x) => throw null; + public S1(int x, object o = null) => throw null; + public object Value => throw null; +} + +class Program +{ + static S1 Test1(int x) + { +#line 14 + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (14,16): error CS0030: Cannot convert type 'int' to 'S1' + // return (S1)x; + Diagnostic(ErrorCode.ERR_NoExplicitConv, "(S1)x").WithArguments("int", "S1").WithLocation(14, 16) + ); + } + + [Fact] + public void UnionConversion_34_No_Non_Public() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(byte x) => throw null; + public S1(string x) => throw null; + internal S1(int x) => throw null; + public object Value => throw null; +} + +class Program +{ + static S1 Test1(int x) + { +#line 14 + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (14,16): error CS0030: Cannot convert type 'int' to 'S1' + // return (S1)x; + Diagnostic(ErrorCode.ERR_NoExplicitConv, "(S1)x").WithArguments("int", "S1").WithLocation(14, 16) + ); + } + + [Theory] + [CombinatorialData] + public void UnionConversion_35_No_Ref_Out([CombinatorialValues("ref", "out", "ref readonly")] string refModifier) + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(" + refModifier + @" int x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static S1 Test1(int x) + { +#line 13 + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (13,16): error CS0030: Cannot convert type 'int' to 'S1' + // return (S1)x; + Diagnostic(ErrorCode.ERR_NoExplicitConv, "(S1)x").WithArguments("int", "S1").WithLocation(13, 16) + ); + } + + [Fact] + public void UnionConversion_36_Implicit_ToNullableOfUnion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1().HasValue ? ""[not null] "" : ""null ""); + System.Console.Write(Test2().HasValue ? ""[not null] "" : ""null ""); + System.Console.Write(Test3().HasValue ? ""[not null] "" : ""null ""); + System.Console.Write(Test4().HasValue ? ""[not null] "" : ""null ""); + System.Console.Write(Test5().HasValue ? ""[not null] "" : ""null ""); + } + + static S1? Test1() + { + System.Console.Write(""1-""); + /**/ return 10; /**/ + } + + static S1? Test2() + { + System.Console.Write(""2-""); + return default; + } + + static S1? Test3() + { + System.Console.Write(""3-""); + return default(S1); + } + + static S1? Test4() + { + System.Console.Write(""4-""); + return null; + } + + static S1? Test5() + { + System.Console.Write(""5-""); + return ""11""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + var ten = GetSyntax(tree, "10"); + + var symbolInfo = model.GetSymbolInfo(ten); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + Assert.Null(symbolInfo.Symbol); + Assert.Empty(symbolInfo.CandidateSymbols); + + var typeInfo = model.GetTypeInfo(ten); + Assert.Equal("System.Int32", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("S1?", typeInfo.ConvertedType.ToTestDisplayString()); + + Conversion conversion = model.GetConversion(ten); + Assert.True(conversion.Exists); + Assert.True(conversion.IsValid); + Assert.True(conversion.IsImplicit); + Assert.False(conversion.IsExplicit); + Assert.Equal(ConversionKind.Union, conversion.Kind); + Assert.Equal(LookupResultKind.Viable, conversion.ResultKind); + Assert.True(conversion.IsUnion); + Assert.False(conversion.IsUserDefined); + AssertEx.Equal("S1..ctor(System.Int32 x)", conversion.Method.ToTestDisplayString()); + AssertEx.Equal("S1..ctor(System.Int32 x)", conversion.MethodSymbol.ToTestDisplayString()); + Assert.Null(conversion.BestUserDefinedConversionAnalysis); + Assert.Equal(Conversion.NoConversion, conversion.UserDefinedFromConversion); + Assert.Equal(Conversion.NoConversion, conversion.UserDefinedToConversion); + Assert.NotNull(conversion.BestUnionConversionAnalysis); + Assert.Empty(conversion.OriginalUserDefinedConversions); + Assert.True(conversion.UnderlyingConversions.IsDefault); + Assert.False(conversion.IsArrayIndex); + Assert.False(conversion.IsExtensionMethod); + + CommonConversion commonConversion = conversion.ToCommonConversion(); + + Assert.True(commonConversion.Exists); + Assert.True(commonConversion.IsImplicit); + Assert.True(commonConversion.IsUnion); + Assert.False(commonConversion.IsUserDefined); + AssertEx.Equal("S1..ctor(System.Int32 x)", commonConversion.MethodSymbol.ToTestDisplayString()); + + VerifyOperationTreeForTest(comp, """ +IReturnOperation (OperationKind.Return, Type: null) (Syntax: 'return 10;') + ReturnedValue: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: S1?, IsImplicit) (Syntax: '10') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + IConversionOperation (TryCast: False, Unchecked) (OperatorMethod: S1..ctor(System.Int32 x)) (OperationKind.Conversion, Type: S1, IsImplicit) (Syntax: '10') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False, IsUnion: True) (MethodSymbol: S1..ctor(System.Int32 x)) + Operand: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 10) (Syntax: '10') +"""); + + var verifier = CompileAndVerify(comp, expectedOutput: "1-int {10} [not null] 2-null 3-[not null] 4-null 5-string {11} [not null]").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 23 (0x17) + .maxstack 1 + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldc.i4.s 10 + IL_000c: newobj ""S1..ctor(int)"" + IL_0011: newobj ""S1?..ctor(S1)"" + IL_0016: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 20 (0x14) + .maxstack 1 + .locals init (S1? V_0) + IL_0000: ldstr ""2-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1?"" + IL_0012: ldloc.0 + IL_0013: ret +} +"); + + verifier.VerifyIL("Program.Test3", @" +{ + // Code size 25 (0x19) + .maxstack 1 + .locals init (S1 V_0) + IL_0000: ldstr ""3-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1"" + IL_0012: ldloc.0 + IL_0013: newobj ""S1?..ctor(S1)"" + IL_0018: ret +} +"); + + verifier.VerifyIL("Program.Test4", @" +{ + // Code size 20 (0x14) + .maxstack 1 + .locals init (S1? V_0) + IL_0000: ldstr ""4-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1?"" + IL_0012: ldloc.0 + IL_0013: ret +} +"); + + verifier.VerifyIL("Program.Test5", @" +{ + // Code size 26 (0x1a) + .maxstack 1 + IL_0000: ldstr ""5-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldstr ""11"" + IL_000f: newobj ""S1..ctor(string)"" + IL_0014: newobj ""S1?..ctor(S1)"" + IL_0019: ret +} +"); + } + + [Fact] + public void UnionConversion_37_Cast_ToNullableOfUnion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1().HasValue ? ""[not null] "" : ""null ""); + System.Console.Write(Test2().HasValue ? ""[not null] "" : ""null ""); + System.Console.Write(Test3().HasValue ? ""[not null] "" : ""null ""); + System.Console.Write(Test4().HasValue ? ""[not null] "" : ""null ""); + System.Console.Write(Test5().HasValue ? ""[not null] "" : ""null ""); + } + + static S1? Test1() + { + System.Console.Write(""1-""); + return /**/ (S1?)10 /**/; + } + + static S1? Test2() + { + System.Console.Write(""2-""); + return (S1?)default; + } + + static S1? Test3() + { + System.Console.Write(""3-""); + return (S1?)default(S1); + } + + static S1? Test4() + { + System.Console.Write(""4-""); + return (S1?)null; + } + + static S1? Test5() + { + System.Console.Write(""5-""); + return (S1?)""11""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + var cast = GetSyntax(tree, "(S1?)10"); + + var typeInfo = model.GetTypeInfo(cast); + Assert.Equal("S1?", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("S1?", typeInfo.ConvertedType.ToTestDisplayString()); + + Conversion conversion = model.GetConversion(cast); + Assert.True(conversion.IsIdentity); + + var symbolInfo = model.GetSymbolInfo(cast); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + AssertEx.Equal("S1..ctor(System.Int32 x)", symbolInfo.Symbol.ToTestDisplayString()); + Assert.Empty(symbolInfo.CandidateSymbols); + + VerifyOperationTreeForTest(comp, """ +IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: S1?) (Syntax: '(S1?)10') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + IConversionOperation (TryCast: False, Unchecked) (OperatorMethod: S1..ctor(System.Int32 x)) (OperationKind.Conversion, Type: S1, IsImplicit) (Syntax: '(S1?)10') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False, IsUnion: True) (MethodSymbol: S1..ctor(System.Int32 x)) + Operand: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 10) (Syntax: '10') +"""); + + var ten = GetSyntax(tree, "10"); + + typeInfo = model.GetTypeInfo(ten); + Assert.Equal("System.Int32", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("System.Int32", typeInfo.ConvertedType.ToTestDisplayString()); + + conversion = model.GetConversion(ten); + Assert.True(conversion.IsIdentity); + + var verifier = CompileAndVerify(comp, expectedOutput: "1-int {10} [not null] 2-null 3-[not null] 4-null 5-string {11} [not null]").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 23 (0x17) + .maxstack 1 + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldc.i4.s 10 + IL_000c: newobj ""S1..ctor(int)"" + IL_0011: newobj ""S1?..ctor(S1)"" + IL_0016: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 20 (0x14) + .maxstack 1 + .locals init (S1? V_0) + IL_0000: ldstr ""2-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1?"" + IL_0012: ldloc.0 + IL_0013: ret +} +"); + + verifier.VerifyIL("Program.Test3", @" +{ + // Code size 25 (0x19) + .maxstack 1 + .locals init (S1 V_0) + IL_0000: ldstr ""3-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1"" + IL_0012: ldloc.0 + IL_0013: newobj ""S1?..ctor(S1)"" + IL_0018: ret +} +"); + + verifier.VerifyIL("Program.Test4", @" +{ + // Code size 20 (0x14) + .maxstack 1 + .locals init (S1? V_0) + IL_0000: ldstr ""4-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""S1?"" + IL_0012: ldloc.0 + IL_0013: ret +} +"); + + verifier.VerifyIL("Program.Test5", @" +{ + // Code size 26 (0x1a) + .maxstack 1 + IL_0000: ldstr ""5-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldstr ""11"" + IL_000f: newobj ""S1..ctor(string)"" + IL_0014: newobj ""S1?..ctor(S1)"" + IL_0019: ret +} +"); + } + + [Fact] + public void UnionConversion_38_Standard_Conversion_For_Source_Allowed_ToNullableOfUnion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) => throw null; + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(15); + Test2(16); + } + + static S1? Test1(byte x1) + { + return x1; + } + + static S1? Test2(byte x2) + { + return (S1?)x2; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "int {15} int {16}").VerifyDiagnostics(); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + var x1 = GetSyntax(tree, "x1"); + + var symbolInfo = model.GetSymbolInfo(x1); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + AssertEx.Equal("System.Byte x1", symbolInfo.Symbol.ToTestDisplayString()); + Assert.Empty(symbolInfo.CandidateSymbols); + + var typeInfo = model.GetTypeInfo(x1); + Assert.Equal("System.Byte", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("S1?", typeInfo.ConvertedType.ToTestDisplayString()); + + Conversion conversion = model.GetConversion(x1); + Assert.True(conversion.IsUnion); + Assert.False(conversion.IsUserDefined); + AssertEx.Equal("S1..ctor(System.Int32 x)", conversion.Method.ToTestDisplayString()); + Assert.Null(conversion.BestUserDefinedConversionAnalysis); + Assert.Equal(Conversion.NoConversion, conversion.UserDefinedFromConversion); + Assert.Equal(Conversion.NoConversion, conversion.UserDefinedToConversion); + Assert.NotNull(conversion.BestUnionConversionAnalysis); + Assert.Empty(conversion.OriginalUserDefinedConversions); + Assert.True(conversion.UnderlyingConversions.IsDefault); + + CommonConversion commonConversion = conversion.ToCommonConversion(); + + Assert.True(commonConversion.Exists); + Assert.True(commonConversion.IsImplicit); + Assert.True(commonConversion.IsUnion); + Assert.False(commonConversion.IsUserDefined); + AssertEx.Equal("S1..ctor(System.Int32 x)", commonConversion.MethodSymbol.ToTestDisplayString()); + + VerifyOperationTreeForNode(comp, model, GetSyntax(tree, "return x1;"), """ +IReturnOperation (OperationKind.Return, Type: null) (Syntax: 'return x1;') + ReturnedValue: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: S1?, IsImplicit) (Syntax: 'x1') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + IConversionOperation (TryCast: False, Unchecked) (OperatorMethod: S1..ctor(System.Int32 x)) (OperationKind.Conversion, Type: S1, IsImplicit) (Syntax: 'x1') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False, IsUnion: True) (MethodSymbol: S1..ctor(System.Int32 x)) + Operand: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Int32, IsImplicit) (Syntax: 'x1') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: True, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + IParameterReferenceOperation: x1 (OperationKind.ParameterReference, Type: System.Byte) (Syntax: 'x1') +"""); + + var x2 = GetSyntax(tree, "x2"); + + symbolInfo = model.GetSymbolInfo(x2); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + AssertEx.Equal("System.Byte x2", symbolInfo.Symbol.ToTestDisplayString()); + Assert.Empty(symbolInfo.CandidateSymbols); + + typeInfo = model.GetTypeInfo(x2); + Assert.Equal("System.Byte", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("System.Byte", typeInfo.ConvertedType.ToTestDisplayString()); + + conversion = model.GetConversion(x2); + Assert.True(conversion.IsIdentity); + Assert.False(conversion.IsUnion); + Assert.False(conversion.IsUserDefined); + + var cast = GetSyntax(tree, "(S1?)x2"); + + symbolInfo = model.GetSymbolInfo(cast); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + AssertEx.Equal("S1..ctor(System.Int32 x)", symbolInfo.Symbol.ToTestDisplayString()); + Assert.Empty(symbolInfo.CandidateSymbols); + + typeInfo = model.GetTypeInfo(cast); + Assert.Equal("S1?", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("S1?", typeInfo.ConvertedType.ToTestDisplayString()); + + VerifyOperationTreeForNode(comp, model, GetSyntax(tree, "return (S1?)x2;"), """ +IReturnOperation (OperationKind.Return, Type: null) (Syntax: 'return (S1?)x2;') + ReturnedValue: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: S1?) (Syntax: '(S1?)x2') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + IConversionOperation (TryCast: False, Unchecked) (OperatorMethod: S1..ctor(System.Int32 x)) (OperationKind.Conversion, Type: S1, IsImplicit) (Syntax: '(S1?)x2') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: False, IsReference: False, IsUserDefined: False, IsUnion: True) (MethodSymbol: S1..ctor(System.Int32 x)) + Operand: + IConversionOperation (TryCast: False, Unchecked) (OperationKind.Conversion, Type: System.Int32, IsImplicit) (Syntax: '(S1?)x2') + Conversion: CommonConversion (Exists: True, IsIdentity: False, IsNumeric: True, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Operand: + IParameterReferenceOperation: x2 (OperationKind.ParameterReference, Type: System.Byte) (Syntax: 'x2') +"""); + } + + [Fact] + public void UnionConversion_39_Implicit_UserDefined_Conversion_Wins_ToNullableOfUnion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null; + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public object Value => throw null; + + public static implicit operator S1(int x) + { + System.Console.Write(""implicit operator ""); + return new S1(x.ToString()); + } +} + +class Program +{ + static void Main() + { + Test1(); + Test2(); + } + + static S1? Test1() + { + return 10; + } + + static S1? Test2() + { + return (S1?)20; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "implicit operator string {10} implicit operator string {20}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_40_Cast_Explicit_UserDefined_Conversion_Wins_ToNullableOfUnion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null; + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public object Value => throw null; + + public static explicit operator S1(int x) + { + System.Console.Write(""explicit operator ""); + return new S1(x.ToString()); + } +} + +class Program +{ + static void Main() + { + Test2(); + } + + static S1? Test2() + { + return (S1?)20; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "explicit operator string {20}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_41_Explicit_UserDefined_Conversion_Loses_ToNullableOfUnion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) + { + System.Console.Write(""int {""); + System.Console.Write(x); + System.Console.Write(""} ""); + } + public S1(string x) => throw null; + public object Value => throw null; + + public static explicit operator S1(int x) => throw null; +} + +class Program +{ + static void Main() + { + Test1(); + } + + static S1? Test1() + { + return 10; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "int {10}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_42_Under_Tuple_Conversion_ToNullableOfUnion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(double x) + { + System.Console.Write(""double {""); + System.Console.Write((int)x); + System.Console.Write(""} ""); + } + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static void Main() + { + Test1((0, 10)); + } + + static (int, S1?) Test1((int, byte) x) + { + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "double {10}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 31 (0x1f) + .maxstack 2 + .locals init (System.ValueTuple V_0) + IL_0000: ldarg.0 + IL_0001: stloc.0 + IL_0002: ldloc.0 + IL_0003: ldfld ""int System.ValueTuple.Item1"" + IL_0008: ldloc.0 + IL_0009: ldfld ""byte System.ValueTuple.Item2"" + IL_000e: conv.r8 + IL_000f: newobj ""S1..ctor(double)"" + IL_0014: newobj ""S1?..ctor(S1)"" + IL_0019: newobj ""System.ValueTuple..ctor(int, S1?)"" + IL_001e: ret +} +"); + } + + [Fact] + public void UnionConversion_43_From_TupleLiteral() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1((int, object) x) + { + System.Console.Write(""(int, object) {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + } + + static S1 Test1() + { + return (10, null); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + var tuple = GetSyntax(tree, "(10, null)"); + + var symbolInfo = model.GetSymbolInfo(tuple); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + Assert.Null(symbolInfo.Symbol); + Assert.Empty(symbolInfo.CandidateSymbols); + + var typeInfo = model.GetTypeInfo(tuple); + Assert.Null(typeInfo.Type); + Assert.Equal("S1", typeInfo.ConvertedType.ToTestDisplayString()); + + Conversion conversion = model.GetConversion(tuple); + Assert.Equal(ConversionKind.Union, conversion.Kind); + Assert.Equal(LookupResultKind.Viable, conversion.ResultKind); + Assert.True(conversion.IsUnion); + AssertEx.Equal("S1..ctor((System.Int32, System.Object) x)", conversion.Method.ToTestDisplayString()); + AssertEx.Equal("S1..ctor((System.Int32, System.Object) x)", conversion.MethodSymbol.ToTestDisplayString()); + + CommonConversion commonConversion = conversion.ToCommonConversion(); + + Assert.True(commonConversion.Exists); + Assert.True(commonConversion.IsImplicit); + Assert.True(commonConversion.IsUnion); + Assert.False(commonConversion.IsUserDefined); + AssertEx.Equal("S1..ctor((System.Int32, System.Object) x)", commonConversion.MethodSymbol.ToTestDisplayString()); + + CompileAndVerify(comp, expectedOutput: "(int, object) {(10, )}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_44_From_TupleLiteral() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1((int, object) x) + { + System.Console.Write(""(int, object) {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + } + + static S1 Test1() + { + return ((byte)10, null); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + var tuple = GetSyntax(tree, "((byte)10, null)"); + + var symbolInfo = model.GetSymbolInfo(tuple); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + Assert.Null(symbolInfo.Symbol); + Assert.Empty(symbolInfo.CandidateSymbols); + + var typeInfo = model.GetTypeInfo(tuple); + Assert.Null(typeInfo.Type); + Assert.Equal("S1", typeInfo.ConvertedType.ToTestDisplayString()); + + Conversion conversion = model.GetConversion(tuple); + Assert.Equal(ConversionKind.Union, conversion.Kind); + Assert.Equal(LookupResultKind.Viable, conversion.ResultKind); + Assert.True(conversion.IsUnion); + AssertEx.Equal("S1..ctor((System.Int32, System.Object) x)", conversion.Method.ToTestDisplayString()); + AssertEx.Equal("S1..ctor((System.Int32, System.Object) x)", conversion.MethodSymbol.ToTestDisplayString()); + + CommonConversion commonConversion = conversion.ToCommonConversion(); + + Assert.True(commonConversion.Exists); + Assert.True(commonConversion.IsImplicit); + Assert.True(commonConversion.IsUnion); + Assert.False(commonConversion.IsUserDefined); + AssertEx.Equal("S1..ctor((System.Int32, System.Object) x)", commonConversion.MethodSymbol.ToTestDisplayString()); + + CompileAndVerify(comp, expectedOutput: "(int, object) {(10, )}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_45_From_TupleLiteral_ToNullableOfUnion() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1((int, object) x) + { + System.Console.Write(""(int, object) {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public S1(string x) + { + System.Console.Write(""string {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + } + + static S1? Test1() + { + return ((byte)10, null); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + var tree = comp.SyntaxTrees.First(); + var model = comp.GetSemanticModel(tree); + var tuple = GetSyntax(tree, "((byte)10, null)"); + + var symbolInfo = model.GetSymbolInfo(tuple); + Assert.Equal(CandidateReason.None, symbolInfo.CandidateReason); + Assert.Null(symbolInfo.Symbol); + Assert.Empty(symbolInfo.CandidateSymbols); + + var typeInfo = model.GetTypeInfo(tuple); + Assert.Null(typeInfo.Type); + Assert.Equal("S1?", typeInfo.ConvertedType.ToTestDisplayString()); + + Conversion conversion = model.GetConversion(tuple); + Assert.Equal(ConversionKind.Union, conversion.Kind); + Assert.Equal(LookupResultKind.Viable, conversion.ResultKind); + Assert.True(conversion.IsUnion); + AssertEx.Equal("S1..ctor((System.Int32, System.Object) x)", conversion.Method.ToTestDisplayString()); + AssertEx.Equal("S1..ctor((System.Int32, System.Object) x)", conversion.MethodSymbol.ToTestDisplayString()); + + CommonConversion commonConversion = conversion.ToCommonConversion(); + + Assert.True(commonConversion.Exists); + Assert.True(commonConversion.IsImplicit); + Assert.True(commonConversion.IsUnion); + Assert.False(commonConversion.IsUserDefined); + AssertEx.Equal("S1..ctor((System.Int32, System.Object) x)", commonConversion.MethodSymbol.ToTestDisplayString()); + + CompileAndVerify(comp, expectedOutput: "(int, object) {(10, )}").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_46_Implicit_Ambiguous_UserDefined_Conversion_Shadows() + { + var src = @" +[System.Runtime.CompilerServices.Union] +public struct S1 +{ + public S1(S2 x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; + + public static implicit operator S1(S2 x) => throw null; +} + +public struct S2 +{ + public static implicit operator S1(S2 x) => throw null; +} + +class Program +{ + static S1 Test2(S2 x) + { +#line 20 + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + + comp.VerifyDiagnostics( + // (20,16): error CS0457: Ambiguous user defined conversions 'S2.implicit operator S1(S2)' and 'S1.implicit operator S1(S2)' when converting from 'S2' to 'S1' + // return x; + Diagnostic(ErrorCode.ERR_AmbigUDConv, "x").WithArguments("S2.implicit operator S1(S2)", "S1.implicit operator S1(S2)", "S2", "S1").WithLocation(20, 16) + ); + } + + [Fact] + public void UnionConversion_47_Cast_Emplicit_Ambiguous_UserDefined_Conversion_Shadows() + { + var src = @" +[System.Runtime.CompilerServices.Union] +public struct S1 +{ + public S1(S2 x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; + + public static explicit operator S1(S2 x) => throw null; +} + +public struct S2 +{ + public static explicit operator S1(S2 x) => throw null; +} + +class Program +{ + static S1 Test2(S2 x) + { +#line 20 + return (S1)x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + + comp.VerifyDiagnostics( + // (20,16): error CS0457: Ambiguous user defined conversions 'S2.explicit operator S1(S2)' and 'S1.explicit operator S1(S2)' when converting from 'S2' to 'S1' + // return (S1)x; + Diagnostic(ErrorCode.ERR_AmbigUDConv, "(S1)x").WithArguments("S2.explicit operator S1(S2)", "S1.explicit operator S1(S2)", "S2", "S1").WithLocation(20, 16) + ); + } + + [Fact] + public void UnionConversion_48_Abstract_Union() + { + var src = @" +[System.Runtime.CompilerServices.Union] +public abstract class C1 +{ + public C1(int x) => throw null; + public C1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static C1 Test1(int x) + { +#line 13 + return new C1(x); + } + + static C1 Test2(int x) + { + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (13,16): error CS0144: Cannot create an instance of the abstract type or interface 'C1' + // return new C1(x); + Diagnostic(ErrorCode.ERR_NoNewAbstract, "new C1(x)").WithArguments("C1").WithLocation(13, 16), + // (18,16): error CS0144: Cannot create an instance of the abstract type or interface 'C1' + // return x; + Diagnostic(ErrorCode.ERR_NoNewAbstract, "x").WithArguments("C1").WithLocation(18, 16) + ); + } + + [Fact] + public void UnionConversion_49_From_Dynamic() + { + var src = @" +[System.Runtime.CompilerServices.Union] +public struct S1 +{ + public S1(int x) => throw null; + public S1(string x) => throw null; + public object Value => throw null; +} + +class Program +{ + static void Main() + { + try + { + Test1(1); + } + catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) + { + System.Console.WriteLine(""RuntimeBinderException caught""); + } + } + + static S1 Test1(dynamic x) + { + return x; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.StandardAndCSharp, options: TestOptions.DebugExe); + // Conversion from dynamic is not a union conversion. + CompileAndVerify(comp, expectedOutput: "RuntimeBinderException caught").VerifyDiagnostics(); + } + + [Fact] + public void UnionConversion_50_NullableConstructorParameter() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int? x) + { + System.Console.Write(""int? {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + Test2(); + } + + static S1 Test1() + { + System.Console.Write(""1-""); + return (int?)null; + } + + static S1 Test2() + { + System.Console.Write(""2-""); + return null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "1-int? {} 2-int? {}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 25 (0x19) + .maxstack 1 + .locals init (int? V_0) + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""int?"" + IL_0012: ldloc.0 + IL_0013: newobj ""S1..ctor(int?)"" + IL_0018: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 25 (0x19) + .maxstack 1 + .locals init (int? V_0) + IL_0000: ldstr ""2-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldloca.s V_0 + IL_000c: initobj ""int?"" + IL_0012: ldloc.0 + IL_0013: newobj ""S1..ctor(int?)"" + IL_0018: ret +} +"); + } + + [Fact] + public void UnionConversion_51_NullableConstructorParameter() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object? _value; + public S1(string? x) + { + System.Console.Write(""string? {""); + System.Console.Write(x); + System.Console.Write(""} ""); + _value = x; + } + public object? Value => _value; +} + +class Program +{ + static void Main() + { + Test1(); + Test2(); + } + + static S1 Test1() + { + System.Console.Write(""1-""); + return (string?)null; + } + + static S1 Test2() + { + System.Console.Write(""2-""); + return null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "1-string? {} 2-string? {}").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 17 (0x11) + .maxstack 1 + IL_0000: ldstr ""1-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: newobj ""S1..ctor(string)"" + IL_0010: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 17 (0x11) + .maxstack 1 + IL_0000: ldstr ""2-"" + IL_0005: call ""void System.Console.Write(string)"" + IL_000a: ldnull + IL_000b: newobj ""S1..ctor(string)"" + IL_0010: ret +} +"); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71773")] + public void UserDefinedCast_RefStruct_Explicit() + { + var source = """ + class C + { + S M1() + { + S s; + s = (S)100; // 1 + return s; + } + + S M2() + { + return (S)200; // 2 + } + + S M3(in int x) + { + S s; + s = (S)x; // 3 + return s; + } + + S M4(in int x) + { + return (S)x; + } + + S M4s(scoped in int x) + { + return (S)x; // 4 + } + + S M5(in int x) + { + S s = (S)x; + return s; + } + + S M5s(scoped in int x) + { + S s = (S)x; + return s; // 5 + } + + S M6() + { + S s = (S)300; + return s; // 6 + } + + void M7(in int x) + { + scoped S s; + s = (S)x; + s = (S)100; + } + } + + [System.Runtime.CompilerServices.Union] + ref struct S + { + public S(in int x) => throw null; + public object Value => throw null; + } + """; + CreateCompilation([source, UnionAttributeSource]).VerifyDiagnostics( + // (6,13): error CS8347: Cannot use a result of 'S.S(in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // s = (S)100; // 1 + Diagnostic(ErrorCode.ERR_EscapeCall, "(S)100").WithArguments("S.S(in int)", "x").WithLocation(6, 13), + // (6,16): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // s = (S)100; // 1 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "100").WithLocation(6, 16), + // (12,16): error CS8347: Cannot use a result of 'S.S(in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return (S)200; // 2 + Diagnostic(ErrorCode.ERR_EscapeCall, "(S)200").WithArguments("S.S(in int)", "x").WithLocation(12, 16), + // (12,19): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // return (S)200; // 2 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "200").WithLocation(12, 19), + // (18,13): error CS8347: Cannot use a result of 'S.S(in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // s = (S)x; // 3 + Diagnostic(ErrorCode.ERR_EscapeCall, "(S)x").WithArguments("S.S(in int)", "x").WithLocation(18, 13), + // (18,16): error CS9077: Cannot return a parameter by reference 'x' through a ref parameter; it can only be returned in a return statement + // s = (S)x; // 3 + Diagnostic(ErrorCode.ERR_RefReturnOnlyParameter, "x").WithArguments("x").WithLocation(18, 16), + // (29,16): error CS8347: Cannot use a result of 'S.S(in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return (S)x; // 4 + Diagnostic(ErrorCode.ERR_EscapeCall, "(S)x").WithArguments("S.S(in int)", "x").WithLocation(29, 16), + // (29,19): error CS9075: Cannot return a parameter by reference 'x' because it is scoped to the current method + // return (S)x; // 4 + Diagnostic(ErrorCode.ERR_RefReturnScopedParameter, "x").WithArguments("x").WithLocation(29, 19), + // (41,16): error CS8352: Cannot use variable 's' in this context because it may expose referenced variables outside of their declaration scope + // return s; // 5 + Diagnostic(ErrorCode.ERR_EscapeVariable, "s").WithArguments("s").WithLocation(41, 16), + // (47,16): error CS8352: Cannot use variable 's' in this context because it may expose referenced variables outside of their declaration scope + // return s; // 6 + Diagnostic(ErrorCode.ERR_EscapeVariable, "s").WithArguments("s").WithLocation(47, 16)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71773")] + public void UserDefinedCast_RefStruct_Implicit() + { + var source = """ + class C + { + S M1() + { + S s; + s = 100; // 1 + return s; + } + + S M2() + { + return 200; // 2 + } + + S M3(in int x) + { + S s; + s = x; // 3 + return s; + } + + S M4(in int x) + { + return x; + } + + S M4s(scoped in int x) + { + return x; // 4 + } + + S M5(in int x) + { + S s = x; + return s; + } + + S M5s(scoped in int x) + { + S s = x; + return s; // 5 + } + + S M6() + { + S s = 300; + return s; // 6 + } + + void M7(in int x) + { + scoped S s; + s = x; + s = 100; + } + } + + [System.Runtime.CompilerServices.Union] + ref struct S + { + public S(in int x) => throw null; + public object Value => throw null; + } + """; + CreateCompilation([source, UnionAttributeSource]).VerifyDiagnostics( + // (6,13): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // s = 100; // 1 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "100").WithLocation(6, 13), + // (6,13): error CS8347: Cannot use a result of 'S.S(in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // s = 100; // 1 + Diagnostic(ErrorCode.ERR_EscapeCall, "100").WithArguments("S.S(in int)", "x").WithLocation(6, 13), + // (12,16): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // return 200; // 2 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "200").WithLocation(12, 16), + // (12,16): error CS8347: Cannot use a result of 'S.S(in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return 200; // 2 + Diagnostic(ErrorCode.ERR_EscapeCall, "200").WithArguments("S.S(in int)", "x").WithLocation(12, 16), + // (18,13): error CS9077: Cannot return a parameter by reference 'x' through a ref parameter; it can only be returned in a return statement + // s = x; // 3 + Diagnostic(ErrorCode.ERR_RefReturnOnlyParameter, "x").WithArguments("x").WithLocation(18, 13), + // (18,13): error CS8347: Cannot use a result of 'S.S(in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // s = x; // 3 + Diagnostic(ErrorCode.ERR_EscapeCall, "x").WithArguments("S.S(in int)", "x").WithLocation(18, 13), + // (29,16): error CS9075: Cannot return a parameter by reference 'x' because it is scoped to the current method + // return x; // 4 + Diagnostic(ErrorCode.ERR_RefReturnScopedParameter, "x").WithArguments("x").WithLocation(29, 16), + // (29,16): error CS8347: Cannot use a result of 'S.S(in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return x; // 4 + Diagnostic(ErrorCode.ERR_EscapeCall, "x").WithArguments("S.S(in int)", "x").WithLocation(29, 16), + // (41,16): error CS8352: Cannot use variable 's' in this context because it may expose referenced variables outside of their declaration scope + // return s; // 5 + Diagnostic(ErrorCode.ERR_EscapeVariable, "s").WithArguments("s").WithLocation(41, 16), + // (47,16): error CS8352: Cannot use variable 's' in this context because it may expose referenced variables outside of their declaration scope + // return s; // 6 + Diagnostic(ErrorCode.ERR_EscapeVariable, "s").WithArguments("s").WithLocation(47, 16)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71773")] + public void UserDefinedCast_RefStructArgument() + { + var source = """ + class C + { + S2 M1() + { + int x = 1; + S1 s1 = (S1)x; + return (S2)s1; // 1 + } + } + + ref struct S1 + { + public static implicit operator S1(in int x) => throw null; + } + + [System.Runtime.CompilerServices.Union] + ref struct S2 + { + public S2(S1 s1) => throw null; + public object Value => throw null; + } + """; + CreateCompilation([source, UnionAttributeSource]).VerifyDiagnostics( + // (7,16): error CS8347: Cannot use a result of 'S2.S2(S1)' in this context because it may expose variables referenced by parameter 's1' outside of their declaration scope + // return (S2)s1; // 1 + Diagnostic(ErrorCode.ERR_EscapeCall, "(S2)s1").WithArguments("S2.S2(S1)", "s1").WithLocation(7, 16), + // (7,20): error CS8352: Cannot use variable 's1' in this context because it may expose referenced variables outside of their declaration scope + // return (S2)s1; // 1 + Diagnostic(ErrorCode.ERR_EscapeVariable, "s1").WithArguments("s1").WithLocation(7, 20)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71773")] + public void UserDefinedCast_StandardImplicitConversion_Input() + { + var source = """ + class C + { + S M1() + { + S s; + s = 100; // 1 + return s; + } + + S M2() + { + return 200; // 2 + } + + S M3(in int x) + { + S s; + s = x; // 3 + return s; + } + + S M4(in int x) + { + return x; // 4 + } + + S M4s(scoped in int x) + { + return x; // 5 + } + + S M5(in int x) + { + S s = x; + return s; // 6 + } + + S M5s(scoped in int x) + { + S s = x; + return s; // 7 + } + + S M6() + { + S s = 300; + return s; // 8 + } + } + + [System.Runtime.CompilerServices.Union] + ref struct S + { + public S(in int? x) => throw null; + public object Value => throw null; + } + """; + CreateCompilation([source, UnionAttributeSource]).VerifyDiagnostics( + // (6,13): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // s = 100; // 1 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "100").WithLocation(6, 13), + // (6,13): error CS8347: Cannot use a result of 'S.S(in int?)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // s = 100; // 1 + Diagnostic(ErrorCode.ERR_EscapeCall, "100").WithArguments("S.S(in int?)", "x").WithLocation(6, 13), + // (12,16): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // return 200; // 2 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "200").WithLocation(12, 16), + // (12,16): error CS8347: Cannot use a result of 'S.S(in int?)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return 200; // 2 + Diagnostic(ErrorCode.ERR_EscapeCall, "200").WithArguments("S.S(in int?)", "x").WithLocation(12, 16), + // (18,13): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // s = x; // 3 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "x").WithLocation(18, 13), + // (18,13): error CS8347: Cannot use a result of 'S.S(in int?)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // s = x; // 3 + Diagnostic(ErrorCode.ERR_EscapeCall, "x").WithArguments("S.S(in int?)", "x").WithLocation(18, 13), + // (24,16): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // return x; // 4 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "x").WithLocation(24, 16), + // (24,16): error CS8347: Cannot use a result of 'S.S(in int?)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return x; // 4 + Diagnostic(ErrorCode.ERR_EscapeCall, "x").WithArguments("S.S(in int?)", "x").WithLocation(24, 16), + // (29,16): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // return x; // 5 + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "x").WithLocation(29, 16), + // (29,16): error CS8347: Cannot use a result of 'S.S(in int?)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return x; // 5 + Diagnostic(ErrorCode.ERR_EscapeCall, "x").WithArguments("S.S(in int?)", "x").WithLocation(29, 16), + // (35,16): error CS8352: Cannot use variable 's' in this context because it may expose referenced variables outside of their declaration scope + // return s; // 6 + Diagnostic(ErrorCode.ERR_EscapeVariable, "s").WithArguments("s").WithLocation(35, 16), + // (41,16): error CS8352: Cannot use variable 's' in this context because it may expose referenced variables outside of their declaration scope + // return s; // 7 + Diagnostic(ErrorCode.ERR_EscapeVariable, "s").WithArguments("s").WithLocation(41, 16), + // (47,16): error CS8352: Cannot use variable 's' in this context because it may expose referenced variables outside of their declaration scope + // return s; // 8 + Diagnostic(ErrorCode.ERR_EscapeVariable, "s").WithArguments("s").WithLocation(47, 16)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71773")] + public void UserDefinedCast_Call() + { + var source = """ + class C + { + S M1(int x) + { + return M2(x); + } + + S M2(S s) => s; + } + + [System.Runtime.CompilerServices.Union] + ref struct S + { + public S(in int x) => throw null; + public object Value => throw null; + } + """; + CreateCompilation([source, UnionAttributeSource]).VerifyDiagnostics( + // (5,16): error CS8347: Cannot use a result of 'C.M2(S)' in this context because it may expose variables referenced by parameter 's' outside of their declaration scope + // return M2(x); + Diagnostic(ErrorCode.ERR_EscapeCall, "M2(x)").WithArguments("C.M2(S)", "s").WithLocation(5, 16), + // (5,19): error CS8166: Cannot return a parameter by reference 'x' because it is not a ref parameter + // return M2(x); + Diagnostic(ErrorCode.ERR_RefReturnParameter, "x").WithArguments("x").WithLocation(5, 19), + // (5,19): error CS8347: Cannot use a result of 'S.S(in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // return M2(x); + Diagnostic(ErrorCode.ERR_EscapeCall, "x").WithArguments("S.S(in int)", "x").WithLocation(5, 19)); + } + + [Fact] + public void NullableAnalysis_01_State_From_Default() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public S3(string? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1 s = default; + _ = s switch { int => 1, bool => 3 }; + } + + static void Test3() + { +#line 300 + S2 s = default; + _ = s switch { int => 1, bool => 3 }; + } + + static void Test4() + { +#line 400 + S3 s = default; + _ = s switch { int => 1, bool => 3, string => 4 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (101,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(101, 15) + ); + } + + [Fact] + public void NullableAnalysis_02_State_From_Default() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +class S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +class S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1? s = null; + _ = s switch { int => 1, bool => 3 }; + } + + static void Test3() + { +#line 300 + S2? s = null; + _ = s switch { int => 1, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (101,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(101, 15), + // (301,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 15) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_03_State_From_Default([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + + static void Test4(S2 s) + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + + static void Test5(S3 s) + { +#line 500 + _ = s switch { int => 1, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 15) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_04_State_From_Default_PostCondition([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!; + public object? Value => throw null!; +} +class Program +{ + static void Test2(S1 s) + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, NotNullAttributeDefinition]); + + comp.VerifyDiagnostics( + // (200,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 15) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_05_State_From_Constructor([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + var s = new S1(1); + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + var s = new S1(""""); + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + var s = new S1(x); + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + var s = new S1(x); + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + var s = new S1(x); + _ = s switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 15), + // (501,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 15) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_06_State_From_Constructor_PostCondition([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] string? x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + var s = new S1(1); + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + var s = new S1(""""); + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + var s = new S1(x); + _ = s switch { int => 1, string => 2, bool => 3 }; + x.ToString(); + } + + static void Test4(bool x) + { +#line 400 + var s = new S1(x); + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + var s = new S1(x); + _ = s switch { int => 1, string => 2, bool => 3 }; + x.Value.ToString(); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, NotNullAttributeDefinition]); + comp.VerifyDiagnostics(); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_07_State_From_Conversion([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1 s = 1; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + S1 s = """"; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + S1 s = x; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + S1 s = x; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + S1 s = x; + _ = s switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 15), + // (501,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 15) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_08_State_From_Conversion_PostCondition([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] string? x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1 s = 1; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + S1 s = """"; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + S1 s = x; + _ = s switch { int => 1, string => 2, bool => 3 }; + x.ToString(); + } + + static void Test4(bool x) + { +#line 400 + S1 s = x; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + S1 s = x; + _ = s switch { int => 1, string => 2, bool => 3 }; + x.Value.ToString(); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, NotNullAttributeDefinition]); + comp.VerifyDiagnostics(); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_09_State_From_Conversion_TupleLiteral([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + (S1, int) s = (1, 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + (S1, int) s = ("""", 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + (S1, int) s = (x, 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + (S1, int) s = (x, 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + (S1, int) s = (x, 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 21), + // (501,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 21) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_10_State_From_Conversion_TupleLiteral_PostCondition([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] string? x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + (S1, int) s = (1, 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + (S1, int) s = ("""", 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + (S1, int) s = (x, 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + x.ToString(); + } + + static void Test4(bool x) + { +#line 400 + (S1, int) s = (x, 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + (S1, int) s = (x, 1); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + x.Value.ToString(); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, NotNullAttributeDefinition]); + comp.VerifyDiagnostics(); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_11_State_From_Conversion_TupleValue([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1((int, int) x) + { +#line 100 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2((string, int) x) + { +#line 200 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3((string?, int) x) + { +#line 300 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4((bool, int) x) + { +#line 400 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5((bool?, int) x) + { +#line 500 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 21), + // (501,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 21) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_12_State_From_Conversion_TupleValue_PostCondition([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] string? x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1((int, int) x) + { +#line 100 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2((string, int) x) + { +#line 200 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3((string?, int) x) + { +#line 300 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + x.Item1.ToString(); + } + + static void Test4((bool, int) x) + { +#line 400 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5((bool?, int) x) + { +#line 500 + (S1, int) s = x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + x.Item1.Value.ToString(); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, NotNullAttributeDefinition]); + comp.VerifyDiagnostics(); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_13_State_From_Conversion_Cast([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1 s = (S1)1; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + S1 s = (S1)""""; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + S1 s = (S1)x; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + S1 s = (S1)x; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + S1 s = (S1)x; + _ = s switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 15), + // (501,15): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 15) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_14_State_From_Conversion_Cast_PostCondition([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] string? x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1 s = (S1)1; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + S1 s = (S1)""""; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + S1 s = (S1)x; + _ = s switch { int => 1, string => 2, bool => 3 }; + x.ToString(); + } + + static void Test4(bool x) + { +#line 400 + S1 s = (S1)x; + _ = s switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + S1 s = (S1)x; + _ = s switch { int => 1, string => 2, bool => 3 }; + x.Value.ToString(); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, NotNullAttributeDefinition]); + comp.VerifyDiagnostics(); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_15_State_From_Conversion_Cast_TupleLiteral([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + (S1, int) s = ((S1, int))(1, 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + (S1, int) s = ((S1, int))("""", 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + (S1, int) s = ((S1, int))(x, 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + (S1, int) s = ((S1, int))(x, 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + (S1, int) s = ((S1, int))(x, 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 21), + // (501,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 21) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_16_State_From_Conversion_Cast_TupleLiteral_PostCondition([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] string? x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + (S1, int) s = ((S1, int))(1, 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + (S1, int) s = ((S1, int))("""", 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + (S1, int) s = ((S1, int))(x, 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + x.ToString(); + } + + static void Test4(bool x) + { +#line 400 + (S1, int) s = ((S1, int))(x, 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + (S1, int) s = ((S1, int))(x, 1.0); + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + x.Value.ToString(); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, NotNullAttributeDefinition]); + comp.VerifyDiagnostics(); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_17_State_From_Conversion_Cast_TupleValue([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1((int, long) x) + { +#line 100 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2((string, long) x) + { +#line 200 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3((string?, long) x) + { +#line 300 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4((bool, long) x) + { +#line 400 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5((bool?, long) x) + { +#line 500 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 21), + // (501,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 21) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_18_State_From_Conversion_Cast_TupleValue_PostCondition([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] string? x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.NotNull] bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1((int, long) x) + { +#line 100 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2((string, long) x) + { +#line 200 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3((string?, long) x) + { +#line 300 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + x.Item1.ToString(); + } + + static void Test4((bool, long) x) + { +#line 400 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5((bool?, long) x) + { +#line 500 + (S1, int) s = ((S1, int))x; + _ = s.Item1 switch { int => 1, string => 2, bool => 3 }; + x.Item1.Value.ToString(); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, NotNullAttributeDefinition]); + comp.VerifyDiagnostics(); + } + + [Fact] + public void NullableAnalysis_19_State_From_Null_Test() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is null) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2 s) + { + if (s is null) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(100, 19), + // (300,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(300, 19) + ); + } + + [Fact] + public void NullableAnalysis_20_State_From_Null_Test_Class() + { + var src1 = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +class S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +class S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is null) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2 s) + { + if (s is null) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp1 = CreateCompilation([src1, UnionAttributeSource]); + + comp1.VerifyDiagnostics( + // (100,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(100, 19), + // (300,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(300, 19) + ); + + var src2 = @" +#nullable enable + +class S1 +{ + public object? Value => throw null!; +} + +class S2 +{ + public object Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + _ = s.Value; + if (s is null or { Value: null }) + { +#line 1000 + _ = s switch { { Value: {} } => 1 }; + } + else + { +#line 2000 + _ = s switch { { Value: {} } => 1 }; + } + } + + static void Test4(S2 s) + { + _ = s.Value; + if (s is null or { Value: null }) + { +#line 3000 + _ = s switch { { Value: {} } => 1 }; + } + else + { +#line 4000 + _ = s switch { { Value: {} } => 1 }; + } + } +} +"; + var comp2 = CreateCompilation(src2); + + comp2.VerifyDiagnostics( + // (1000,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { { Value: {} } => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(1000, 19), + // (2000,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { { Value: {} } => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(2000, 19), + // (3000,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { { Value: {} } => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(3000, 19), + // (4000,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { { Value: {} } => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(4000, 19) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_21_State_From_NotNull_Test([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is not null) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2 s) + { + if (s is not null) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 19), + // (400,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(400, 19) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_22_State_From_Type_Test([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is int) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2 s) + { + if (s is int) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test5(S3 s) + { + if (s is int) + { +#line 500 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 600 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 19) + ); + } + + [Fact] + public void NullableAnalysis_23_State_From_NotType_Test() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is not int) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2 s) + { + if (s is not int) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test5(S3 s) + { + if (s is not int) + { +#line 500 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 600 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(100, 19) + ); + } + + [Fact] + public void NullableAnalysis_24_State_From_NotType_Test_Class() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +class S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +class S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +class S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is not int) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2 s) + { + if (s is not int) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test5(S3 s) + { + if (s is not int) + { +#line 500 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 600 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + + // https://github.com/dotnet/roslyn/issues/82636: This is another case of behavior consistent with explicit property patterns. See below + comp.VerifyDiagnostics( + // (100,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(100, 19), + // (200,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 19) + ); + + var src2 = @" +#nullable enable + +class S1 +{ + public object? Value => throw null!; +} +class Program +{ + static void Test2(S1 s) + { + _ = s.Value; + if (s is { Value: not int }) + { +#line 1000 + _ = s switch { { Value: {} } => 1 }; + } + else + { +#line 2000 + _ = s switch { { Value: {} } => 1 }; + } + } +} +"; + var comp2 = CreateCompilation(src2); + + comp2.VerifyDiagnostics( + // (1000,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { { Value: {} } => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(1000, 19), + // (2000,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { { Value: {} } => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(2000, 19) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_25_State_From_Value_Test([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is 1) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2 s) + { + if (s is 1) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test5(S3 s) + { + if (s is 1) + { +#line 500 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 600 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 19) + ); + } + + [Fact] + public void NullableAnalysis_26_State_From_NotValue_Test() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is not 1) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2 s) + { + if (s is not 1) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test5(S3 s) + { + if (s is not 1) + { +#line 500 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 600 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(100, 19) + ); + } + + [Fact] + public void NullableAnalysis_27_State_From_NotValue_Test_Class() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +class S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +class S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +class S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is not 1) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2 s) + { + if (s is not 1) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test5(S3 s) + { + if (s is not 1) + { +#line 500 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 600 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + + // https://github.com/dotnet/roslyn/issues/82636: This is another case of behavior consistent with explicit property patterns. See below + comp.VerifyDiagnostics( + // (100,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(100, 19), + // (200,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 19) + ); + + var src2 = @" +#nullable enable + +class S1 +{ + public object? Value => throw null!; +} +class Program +{ + static void Test2(S1 s) + { + _ = s.Value; + if (s is { Value: not 1 }) + { +#line 1000 + _ = s switch { { Value: {} } => 1 }; + } + else + { +#line 2000 + _ = s switch { { Value: {} } => 1 }; + } + } +} +"; + var comp2 = CreateCompilation(src2); + + comp2.VerifyDiagnostics( + // (1000,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { { Value: {} } => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(1000, 19), + // (2000,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { { Value: {} } => 1 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(2000, 19) + ); + } + + [Fact] + public void NullableAnalysis_28() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(OtherProp))] + public object? Value => throw null!; + public string? OtherProp => throw null!; +} + +public interface I1 +{ + object? Value { get; } +} + +struct S2 : I1 +{ + public S2(int x) => throw null!; + public S2(bool? x) => throw null!; + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(OtherProp))] + object? I1.Value => throw null!; + public string? OtherProp => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { +#line 200 + _ = s switch { bool => s.OtherProp.ToString(), _ => """" }; + } + + static void Test3(S2 s) + { +#line 300 + _ = s switch { I1 and { Value: bool } => s.OtherProp.ToString(), _ => """" }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, MemberNotNullAttributeDefinition]); + + comp.VerifyDiagnostics( + // (300,25): hidden CS9335: The pattern is redundant. + // _ = s switch { I1 and { Value: bool } => s.OtherProp.ToString(), _ => "" }; + Diagnostic(ErrorCode.HDN_RedundantPattern, "I1").WithLocation(300, 25), + // (300,51): warning CS8602: Dereference of a possibly null reference. + // _ = s switch { I1 and { Value: bool } => s.OtherProp.ToString(), _ => "" }; + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s.OtherProp").WithLocation(300, 51) + ); + } + + [Fact] + public void NullableAnalysis_29() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(OtherProp))] + public object? Value => throw null!; + public string? OtherProp => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null!; + public S2(bool? x) => throw null!; + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(OtherProp))] + public object? Value => throw null!; + public string? OtherProp => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { +#line 200 + _ = s switch { bool => s.OtherProp.ToString(), _ => """" }; + } + + static void Test3(S2 s) + { +#line 300 + _ = s switch { bool => s.OtherProp.ToString(), _ => """" }; + } +} +"; + var comp = CreateCompilation([src, MemberNotNullAttributeDefinition, UnionAttributeSource]); + + comp.VerifyDiagnostics(); + } + + [Fact] + public void NullableAnalysis_30() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + [System.Diagnostics.CodeAnalysis.MemberNotNull(nameof(OtherProp))] + public object? Value => throw null!; + public string? OtherProp => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { +#line 200 + _ = s switch { bool => s.OtherProp.ToString(), _ => """" }; + } +} +"; + var comp = CreateCompilation([src, MemberNotNullAttributeDefinition, UnionAttributeSource]); + + comp.VerifyDiagnostics(); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_31_Conversion_Value_Check([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1(string x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1(string? x) + { +#line 100 + S1 s = x; + x.ToString(); + } + + static void Test2(string? x) + { +#line 200 + var s = new S1(x); + x.ToString(); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,16): warning CS8604: Possible null reference argument for parameter 'x' in 'S1.S1(string x)'. + // S1 s = x; + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "x").WithArguments("x", "S1.S1(string x)").WithLocation(100, 16), + // (200,24): warning CS8604: Possible null reference argument for parameter 'x' in 'S1.S1(string x)'. + // var s = new S1(x); + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "x").WithArguments("x", "S1.S1(string x)").WithLocation(200, 24) + ); + } + + [Theory] + [CombinatorialData] + public void NullableAnalysis_32_Conversion_Value_Check([CombinatorialValues("class", "struct")] string typeKind) + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +" + typeKind + @" S1 +{ + public S1([System.Diagnostics.CodeAnalysis.DisallowNull] string? x) => throw null!; + public S1([System.Diagnostics.CodeAnalysis.DisallowNull] bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1(string? x) + { +#line 100 + S1 s = x; + x.ToString(); + } + + static void Test2(string? x) + { +#line 200 + var s = new S1(x); + x.ToString(); + } + + static void Test3(bool? x) + { +#line 300 + S1 s = x; + x.Value.ToString(); + } + + static void Test4(bool? x) + { +#line 400 + var s = new S1(x); + x.Value.ToString(); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, DisallowNullAttributeDefinition]); + comp.VerifyDiagnostics( + // (100,16): warning CS8604: Possible null reference argument for parameter 'x' in 'S1.S1(string? x)'. + // S1 s = x; + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "x").WithArguments("x", "S1.S1(string? x)").WithLocation(100, 16), + // (200,24): warning CS8604: Possible null reference argument for parameter 'x' in 'S1.S1(string? x)'. + // var s = new S1(x); + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "x").WithArguments("x", "S1.S1(string? x)").WithLocation(200, 24), + // (300,16): warning CS8607: A possible null value may not be used for a type marked with [NotNull] or [DisallowNull] + // S1 s = x; + Diagnostic(ErrorCode.WRN_DisallowNullAttributeForbidsMaybeNullAssignment, "x").WithLocation(300, 16), + // (400,24): warning CS8607: A possible null value may not be used for a type marked with [NotNull] or [DisallowNull] + // var s = new S1(x); + Diagnostic(ErrorCode.WRN_DisallowNullAttributeForbidsMaybeNullAssignment, "x").WithLocation(400, 24) + ); + } + + [Fact] + public void NullableAnalysis_33_State_From_Default_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1? s = default(S1); + _ = s.Value switch { int => 1, bool => 3 }; + } + + static void Test3() + { +#line 300 + S2? s = default(S2); + _ = s.Value switch { int => 1, bool => 3 }; + } + + static void Test4() + { +#line 400 + S3? s = default(S3); + _ = s.Value switch { int => 1, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (101,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(101, 21) + ); + } + + [Fact] + public void NullableAnalysis_34_State_From_Default_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S3 +{ + public S3(int? x) => throw null!; + public S3(bool? x) => throw null!; + public object Value => throw null!; +} + +class Program +{ + static void Test2(S1? s) + { + if (s is null) return; +#line 200 + _ = s.Value switch { int => 1, bool => 3 }; + } + + static void Test4(S2? s) + { + if (s is null) return; +#line 400 + _ = s.Value switch { int => 1, bool => 3 }; + } + + static void Test5(S3? s) + { + if (s is null) return; +#line 500 + _ = s.Value switch { int => 1, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics(); + } + + [Fact] + public void NullableAnalysis_35_State_From_Constructor_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1? s = new S1(1); + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + S1? s = new S1(""""); + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + S1? s = new S1(x); + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + S1? s = new S1(x); + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + S1? s = new S1(x); + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 21), + // (501,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 21) + ); + } + + [Fact] + public void NullableAnalysis_36_State_From_Conversion_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1? s = 1; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + S1? s = """"; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + S1? s = x; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + S1? s = x; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + S1? s = x; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 21), + // (501,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 21) + ); + } + + [Fact] + public void NullableAnalysis_37_State_From_Conversion_TupleLiteral_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + (S1?, int) s = (1, 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + (S1?, int) s = ("""", 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + (S1?, int) s = (x, 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + (S1?, int) s = (x, 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + (S1?, int) s = (x, 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,27): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 27), + // (501,27): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 27) + ); + } + + [Fact] + public void NullableAnalysis_38_State_From_Conversion_TupleValue_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1((int, int) x) + { +#line 100 + (S1?, int) s = x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2((string, int) x) + { +#line 200 + (S1?, int) s = x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3((string?, int) x) + { +#line 300 + (S1?, int) s = x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4((bool, int) x) + { +#line 400 + (S1?, int) s = x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5((bool?, int) x) + { +#line 500 + (S1?, int) s = x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,27): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 27), + // (501,27): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 27) + ); + } + + [Fact] + public void NullableAnalysis_39_State_From_Conversion_Cast_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + S1? s = (S1?)1; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + S1? s = (S1?)""""; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + S1? s = (S1?)x; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + S1? s = (S1?)x; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + S1? s = (S1?)x; + _ = s.Value switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 21), + // (501,21): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 21) + ); + } + + [Fact] + public void NullableAnalysis_40_State_From_Conversion_Cast_TupleLiteral_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1() + { +#line 100 + (S1?, int) s = ((S1?, int))(1, 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2() + { +#line 200 + (S1?, int) s = ((S1?, int))("""", 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3(string? x) + { +#line 300 + (S1?, int) s = ((S1?, int))(x, 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4(bool x) + { +#line 400 + (S1?, int) s = ((S1?, int))(x, 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5(bool? x) + { +#line 500 + (S1?, int) s = ((S1?, int))(x, 1); + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,27): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 27), + // (501,27): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 27) + ); + } + + [Fact] + public void NullableAnalysis_41_State_From_Conversion_Cast_TupleValue_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1((int, int) x) + { +#line 100 + (S1?, int) s = ((S1?, int))x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test2((string, int) x) + { +#line 200 + (S1?, int) s = ((S1?, int))x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test3((string?, int) x) + { +#line 300 + (S1?, int) s = ((S1?, int))x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test4((bool, int) x) + { +#line 400 + (S1?, int) s = ((S1?, int))x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } + + static void Test5((bool?, int) x) + { +#line 500 + (S1?, int) s = ((S1?, int))x; + _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (301,27): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 27), + // (501,27): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Item1.Value switch { int => 1, string => 2, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(501, 27) + ); + } + + [Fact] + public void NullableAnalysis_42_State_From_Null_Test_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test2(S1? s0) + { + if (s0 is null) return; + + if (s0.Value is null) + { + var s = s0; +#line 100 + _ = s.Value switch { int => 1, bool => 3 }; + } + else + { + var s = s0; +#line 200 + _ = s.Value switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2? s0) + { + if (s0 is null) return; + + if (s0.Value is null) + { + var s = s0; +#line 300 + _ = s.Value switch { int => 1, bool => 3 }; + } + else + { + var s = s0; +#line 400 + _ = s.Value switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,25): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(100, 25), + // (300,25): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(300, 25) + ); + } + + [Fact] + public void NullableAnalysis_43_State_From_NotNull_Test_NullableOfUnion() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +[System.Runtime.CompilerServices.Union] +struct S2 +{ + public S2(int x) => throw null!; + public S2(bool x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test2(S1? s0) + { + if (s0 is null) return; + + if (s0.Value is not null) + { + var s = s0; +#line 100 + _ = s.Value switch { int => 1, bool => 3 }; + } + else + { + var s = s0; +#line 200 + _ = s.Value switch { int => 1, bool => 3 }; + } + } + + static void Test4(S2? s0) + { + if (s0 is null) return; + + if (s0.Value is not null) + { + var s = s0; +#line 300 + _ = s.Value switch { int => 1, bool => 3 }; + } + else + { + var s = s0; +#line 400 + _ = s.Value switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,25): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 25), + // (400,25): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s.Value switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(400, 25) + ); + } + + [Fact] + public void NullableAnalysis_44_Conversion_Value_Check_ReinferConstructor() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(T x) => throw null!; + public S1(bool x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test1(string? x, string? y) + { + var s = GetS1(y); +#line 100 + s = x; + x.ToString(); + } + + static void Test2(string? x, string y) + { + var s = GetS1(y); +#line 200 + s = x; + x.ToString(); + } + + static S1 GetS1(T x) + { + return default; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (101,9): warning CS8602: Dereference of a possibly null reference. + // x.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "x").WithLocation(101, 9), + // (200,13): warning CS8604: Possible null reference argument for parameter 'x' in 'S1.S1(string x)'. + // s = x; + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "x").WithArguments("x", "S1.S1(string x)").WithLocation(200, 13) + ); + } + + [Fact] + public void NullableAnalysis_45_ValuePropertyOfTheInterfaceIsTargetedNotValuePropertyOfTheType() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s is not null) + { +#line 100 + s.Value.ToString(); + } + else + { +#line 200 + s.Value.ToString(); + } + } + + static void Test4(S1 s) + { + if (s is null) + { +#line 300 + s.Value.ToString(); + } + else + { +#line 400 + s.Value.ToString(); + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + + comp.VerifyDiagnostics( + // (200,13): warning CS8602: Dereference of a possibly null reference. + // s.Value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s.Value").WithLocation(200, 13), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // s.Value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s.Value").WithLocation(300, 13) + ); + } + + [Fact] + public void NonBoxingUnionMatching_01_HasValue_Struct() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool HasValue => _value != null; + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1())); + } + + static bool Test1(S1 u) + { + return u is null; + } + + static bool Test2(S1 u) + { + return u is not null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "FalseTrueTrueFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 11 (0xb) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1.HasValue.get"" + IL_0007: ldc.i4.0 + IL_0008: ceq + IL_000a: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 8 (0x8) + .maxstack 1 + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1.HasValue.get"" + IL_0007: ret +} +"); + } + + [Theory] + [CombinatorialData] + public void NonBoxingUnionMatching_02_HasValue_Struct([CombinatorialValues("internal", "private")] string accessibility) + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + " + accessibility + @" bool HasValue => throw null; + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1())); + } + + static bool Test1(S1 u) + { + return u is null; + } + + static bool Test2(S1 u) + { + return u is not null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "FalseTrueTrueFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 11 (0xb) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: call ""object S1.Value.get"" + IL_0007: ldnull + IL_0008: ceq + IL_000a: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 11 (0xb) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: call ""object S1.Value.get"" + IL_0007: ldnull + IL_0008: cgt.un + IL_000a: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_03_HasValue_Class() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool HasValue => _value != null; + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1(null))); + System.Console.Write(Test1(null)); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1(null))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1 u) + { + return u is null; + } + + static bool Test2(S1 u) + { + return u is not null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "FalseTrueTrueTrueFalseFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 19 (0x13) + .maxstack 1 + .locals init (bool V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""bool S1.HasValue.get"" + IL_0009: brtrue.s IL_000f + IL_000b: ldc.i4.1 + IL_000c: stloc.0 + IL_000d: br.s IL_0011 + IL_000f: ldc.i4.0 + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 12 (0xc) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000a + IL_0003: ldarg.0 + IL_0004: callvirt ""bool S1.HasValue.get"" + IL_0009: ret + IL_000a: ldc.i4.0 + IL_000b: ret +} +"); + } + + [Theory] + [CombinatorialData] + public void NonBoxingUnionMatching_04_HasValue_Class([CombinatorialValues("internal", "private", "protected", "private protected", "protected internal")] string accessibility) + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + " + accessibility + @" bool HasValue => throw null; + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1(null))); + System.Console.Write(Test1(null)); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1(null))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1 u) + { + return u is null; + } + + static bool Test2(S1 u) + { + return u is not null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "FalseTrueTrueTrueFalseFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 19 (0x13) + .maxstack 1 + .locals init (bool V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""object S1.Value.get"" + IL_0009: brtrue.s IL_000f + IL_000b: ldc.i4.1 + IL_000c: stloc.0 + IL_000d: br.s IL_0011 + IL_000f: ldc.i4.0 + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 15 (0xf) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000d + IL_0003: ldarg.0 + IL_0004: callvirt ""object S1.Value.get"" + IL_0009: ldnull + IL_000a: cgt.un + IL_000c: ret + IL_000d: ldc.i4.0 + IL_000e: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_05_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool HasValue => _value != null; + + static void Main() + { + System.Console.Write(Test2(new S1(1))); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(2))); + } + + static bool Test2(S1 u) + { + return u is not null and 1; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseFalse").VerifyDiagnostics(); + + // The IL would be shorter without HasValue, but, I guess, we expect + // non-boxing pattern to be fully implemented if HasValue is present. + // The scenario is somewhat pathological as well, no actual need to have 'not null' + // pattern in the code. + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 37 (0x25) + .maxstack 2 + .locals init (object V_0) + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1.HasValue.get"" + IL_0007: brfalse.s IL_0023 + IL_0009: ldarga.s V_0 + IL_000b: call ""object S1.Value.get"" + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: isinst ""int"" + IL_0017: brfalse.s IL_0023 + IL_0019: ldloc.0 + IL_001a: unbox.any ""int"" + IL_001f: ldc.i4.1 + IL_0020: ceq + IL_0022: ret + IL_0023: ldc.i4.0 + IL_0024: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_06_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + static void Main() + { + System.Console.Write(Test2(new S1(1))); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(""a""))); + } + + static int Test2(S1 u) + { +#line 26 + return u switch { null => 0, not null => 1}; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "HasValue 1HasValue 0HasValue 1").VerifyDiagnostics( + // (26,42): hidden CS9335: The pattern is redundant. + // return u switch { null => 0, not null => 1}; + Diagnostic(ErrorCode.HDN_RedundantPattern, "null").WithLocation(26, 42) + ); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 17 (0x11) + .maxstack 1 + .locals init (int V_0) + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1.HasValue.get"" + IL_0007: brtrue.s IL_000d + IL_0009: ldc.i4.0 + IL_000a: stloc.0 + IL_000b: br.s IL_000f + IL_000d: ldc.i4.1 + IL_000e: stloc.0 + IL_000f: ldloc.0 + IL_0010: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_07_HasValue_Class_Inheritance() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class C1 +{ + private readonly object _value; + public C1(int x) { _value = x; } + public C1(string x) { _value = x; } + public object Value => _value; + public bool HasValue => throw null; // https://github.com/dotnet/roslyn/issues/82636: Inheritance isn't handled yet +} + +[System.Runtime.CompilerServices.Union] +class C2 : C1 +{ + public C2(int x) : base(x) { } + public C2(string x) : base(x) { } + public new object Value => base.Value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C2(1))); + System.Console.Write(Test1(new C2(null))); + System.Console.Write(Test1(null)); + } + + static bool Test1(C2 u) + { + return u is null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "FalseTrueTrue").VerifyDiagnostics(); + } + + [Fact] + public void NonBoxingUnionMatching_08_HasValue_Class_Inheritance() + { + var src = @" +class C0 +{ + public bool HasValue => throw null; // https://github.com/dotnet/roslyn/issues/82636: Inheritance isn't handled yet +} + +[System.Runtime.CompilerServices.Union] +class C1 : C0 +{ + private readonly object _value; + public C1(int x) { _value = x; } + public C1(string x) { _value = x; } + public object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C1(1))); + System.Console.Write(Test1(new C1(null))); + System.Console.Write(Test1(null)); + } + + static bool Test1(C1 u) + { + return u is null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: "FalseTrueTrue").VerifyDiagnostics(); + } + + [Fact] + public void NonBoxingUnionMatching_09_HasValue_Class_Inheritance() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class C1 +{ + protected readonly object _value; + public C1(int x) { _value = x; } + public C1(string x) { _value = x; } + public object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +class C2 : C1 +{ + public C2(int x) : base(x) { } + public C2(string x) : base(x) { } + public bool HasValue => _value != null; + public new object Value => _value; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C2(1))); + System.Console.Write(Test1(new C2(null))); + System.Console.Write(Test1(null)); + } + + static bool Test1(C2 u) + { + return u is null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "FalseTrueTrue").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 19 (0x13) + .maxstack 1 + .locals init (bool V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""bool C2.HasValue.get"" + IL_0009: brtrue.s IL_000f + IL_000b: ldc.i4.1 + IL_000c: stloc.0 + IL_000d: br.s IL_0011 + IL_000f: ldc.i4.0 + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_10_HasValue_Class_Inheritance() + { + var src = @" +[System.Runtime.CompilerServices.Union] +abstract class C1 +{ + protected readonly object _value; + public C1(int x) { _value = x; } + public C1(string x) { _value = x; } + public object Value => _value; + public abstract bool HasValue { get; } +} + +[System.Runtime.CompilerServices.Union] +class C2 : C1 +{ + public C2(int x) : base(x) { } + public C2(string x) : base(x) { } + public override bool HasValue => _value != null; + public new object Value => _value; +} + +[System.Runtime.CompilerServices.Union] +abstract class C3 : C1 +{ + public C3(int x) : base(x) { } + public C3(string x) : base(x) { } + public abstract override bool HasValue { get; } + public new object Value => _value; +} + +class C4 : C3 +{ + public C4(int x) : base(x) { } + public C4(string x) : base(x) { } + public override bool HasValue => _value != null; +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new C2(1))); + System.Console.Write(Test1(new C2(null))); + System.Console.Write(Test1(null)); + + System.Console.Write(Test2(new C2(1))); + System.Console.Write(Test2(new C2(null))); + System.Console.Write(Test2(null)); + + System.Console.Write(Test3(new C4(1))); + System.Console.Write(Test3(new C4(null))); + System.Console.Write(Test3(null)); + } + + static bool Test1(C1 u) + { + return u is null; + } + + static bool Test2(C2 u) + { + return u is null; + } + + static bool Test3(C3 u) + { + return u is null; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "FalseTrueTrueFalseTrueTrueFalseTrueTrue").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 19 (0x13) + .maxstack 1 + .locals init (bool V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""bool C1.HasValue.get"" + IL_0009: brtrue.s IL_000f + IL_000b: ldc.i4.1 + IL_000c: stloc.0 + IL_000d: br.s IL_0011 + IL_000f: ldc.i4.0 + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: ret +} +"); + + verifier.VerifyIL("Program.Test2", @" +{ + // Code size 19 (0x13) + .maxstack 1 + .locals init (bool V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""bool C1.HasValue.get"" + IL_0009: brtrue.s IL_000f + IL_000b: ldc.i4.1 + IL_000c: stloc.0 + IL_000d: br.s IL_0011 + IL_000f: ldc.i4.0 + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: ret +} +"); + + verifier.VerifyIL("Program.Test3", @" +{ + // Code size 19 (0x13) + .maxstack 1 + .locals init (bool V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000b + IL_0003: ldarg.0 + IL_0004: callvirt ""bool C1.HasValue.get"" + IL_0009: brtrue.s IL_000f + IL_000b: ldc.i4.1 + IL_000c: stloc.0 + IL_000d: br.s IL_0011 + IL_000f: ldc.i4.0 + IL_0010: stloc.0 + IL_0011: ldloc.0 + IL_0012: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_11_HasValue_NullableAnalysis() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public bool HasValue => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.HasValue) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (!s.HasValue) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 19), + // (300,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(300, 19) + ); + } + + [Fact] + public void NonBoxingUnionMatching_12_HasValue_NullableAnalysis_Generic() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(T x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public bool HasValue => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.HasValue) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (!s.HasValue) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 19), + // (300,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(300, 19) + ); + } + + [Fact] + public void NonBoxingUnionMatching_13_HasValue_NullableAnalysis_Generic() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public T HasValue => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.HasValue) + { +#line 100 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (!s.HasValue) + { +#line 300 + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(100, 19), + // (200,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(200, 19), + // (300,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(300, 19), + // (400,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(400, 19) + ); + } + + [Fact] + public void NonBoxingUnionMatching_14_NullableAnalysis_ValuePropertyOfTheInterfaceIsTargetedNotValuePropertyOfTheType() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public bool HasValue => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.HasValue) + { +#line 100 + s.Value.ToString(); + } + else + { +#line 200 + s.Value.ToString(); + } + } + + static void Test4(S1 s) + { + if (!s.HasValue) + { +#line 300 + s.Value.ToString(); + } + else + { +#line 400 + s.Value.ToString(); + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + + comp.VerifyDiagnostics( + // (200,13): warning CS8602: Dereference of a possibly null reference. + // s.Value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s.Value").WithLocation(200, 13), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // s.Value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "s.Value").WithLocation(300, 13) + ); + } + + [Fact] + public void NonBoxingUnionMatching_15_TryGetValue_Struct() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool TryGetValue(out int x) { if (_value is int v) { x = v; return true; } x = 0; return false; } + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""a""))); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(""b""))); + } + + static bool Test1(S1 u) + { + return u is int; + } + + static bool Test2(S1 u) + { + return u is not int; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseTrueTrue").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 10 (0xa) + .maxstack 2 + .locals init (int V_0) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out int)"" + IL_0009: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 13 (0xd) + .maxstack 2 + .locals init (int V_0) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out int)"" + IL_0009: ldc.i4.0 + IL_000a: ceq + IL_000c: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_16_TryGetValue_Class() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool TryGetValue(out int x) { if (_value is int v) { x = v; return true; } x = 0; return false; } + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1(null))); + System.Console.Write(Test1(new S1(""a""))); + System.Console.Write(Test1(null)); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1(null))); + System.Console.Write(Test2(new S1(""b""))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1 u) + { + return u is int; + } + + static bool Test2(S1 u) + { + return u is not int; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseFalseTrueTrueFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 14 (0xe) + .maxstack 2 + .locals init (int V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000c + IL_0003: ldarg.0 + IL_0004: ldloca.s V_0 + IL_0006: callvirt ""bool S1.TryGetValue(out int)"" + IL_000b: ret + IL_000c: ldc.i4.0 + IL_000d: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 17 (0x11) + .maxstack 2 + .locals init (int V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_000f + IL_0003: ldarg.0 + IL_0004: ldloca.s V_0 + IL_0006: callvirt ""bool S1.TryGetValue(out int)"" + IL_000b: ldc.i4.0 + IL_000c: ceq + IL_000e: ret + IL_000f: ldc.i4.0 + IL_0010: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_17_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(object x) { _value = x; } + public object Value => _value; + public bool TryGetValue(out object x) { x = _value; return x != null; } + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""a""))); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(""b""))); + } + + static bool Test1(S1 u) + { + return u is object; + } + + static bool Test2(S1 u) + { + return u is not object; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseTrueFalseTrueFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 10 (0xa) + .maxstack 2 + .locals init (object V_0) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out object)"" + IL_0009: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 13 (0xd) + .maxstack 2 + .locals init (object V_0) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out object)"" + IL_0009: ldc.i4.0 + IL_000a: ceq + IL_000c: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_18_TryGetValue_Plus_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(object x) { _value = x; } + public object Value => _value; + public bool TryGetValue(out object x) { x = _value; return x != null; } + public bool HasValue => _value != null; + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""a""))); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(""b""))); + } + + static bool Test1(S1 u) + { + return u is object; + } + + static bool Test2(S1 u) + { + return u is not object; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseTrueFalseTrueFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 8 (0x8) + .maxstack 1 + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1.HasValue.get"" + IL_0007: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 11 (0xb) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1.HasValue.get"" + IL_0007: ldc.i4.0 + IL_0008: ceq + IL_000a: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_19_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(object x) { _value = x; } + public object Value => _value; + public bool HasValue => _value != null; + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""a""))); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(""b""))); + } + + static bool Test1(S1 u) + { + return u is object; + } + + static bool Test2(S1 u) + { + return u is not object; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseTrueFalseTrueFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 8 (0x8) + .maxstack 1 + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1.HasValue.get"" + IL_0007: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 11 (0xb) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1.HasValue.get"" + IL_0007: ldc.i4.0 + IL_0008: ceq + IL_000a: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_20_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(object x) { _value = x; } + public object Value => _value; + public bool HasValue => _value != null; + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""a""))); + } + + static int Test1(S1 u) + { + return u switch { null => 0, int => 1, _ => 2}; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "102").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 37 (0x25) + .maxstack 1 + .locals init (int V_0) + IL_0000: ldarga.s V_0 + IL_0002: call ""bool S1.HasValue.get"" + IL_0007: brfalse.s IL_0019 + IL_0009: ldarga.s V_0 + IL_000b: call ""object S1.Value.get"" + IL_0010: isinst ""int"" + IL_0015: brtrue.s IL_001d + IL_0017: br.s IL_0021 + IL_0019: ldc.i4.0 + IL_001a: stloc.0 + IL_001b: br.s IL_0023 + IL_001d: ldc.i4.1 + IL_001e: stloc.0 + IL_001f: br.s IL_0023 + IL_0021: ldc.i4.2 + IL_0022: stloc.0 + IL_0023: ldloc.0 + IL_0024: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_21_TryGetValue_Struct() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool TryGetValue(out string x) { x = _value as string; return x != null; } + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""a""))); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(""b""))); + } + + static bool Test1(S1 u) + { + return u is ""a""; + } + + static bool Test2(S1 u) + { + return u is not ""b""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "FalseFalseTrueTrueTrueFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 25 (0x19) + .maxstack 2 + .locals init (string V_0) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out string)"" + IL_0009: brfalse.s IL_0017 + IL_000b: ldloc.0 + IL_000c: ldstr ""a"" + IL_0011: call ""bool string.op_Equality(string, string)"" + IL_0016: ret + IL_0017: ldc.i4.0 + IL_0018: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 32 (0x20) + .maxstack 2 + .locals init (string V_0, + bool V_1) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out string)"" + IL_0009: brfalse.s IL_0018 + IL_000b: ldloc.0 + IL_000c: ldstr ""b"" + IL_0011: call ""bool string.op_Equality(string, string)"" + IL_0016: brtrue.s IL_001c + IL_0018: ldc.i4.1 + IL_0019: stloc.1 + IL_001a: br.s IL_001e + IL_001c: ldc.i4.0 + IL_001d: stloc.1 + IL_001e: ldloc.1 + IL_001f: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_22_TryGetValue_Class() + { + var src = @" +[System.Runtime.CompilerServices.Union] +class S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool TryGetValue(out string x) { x = _value as string; return x != null; } + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1(null))); + System.Console.Write(Test1(new S1(""a""))); + System.Console.Write(Test1(null)); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1(null))); + System.Console.Write(Test2(new S1(""b""))); + System.Console.Write(Test2(null)); + } + + static bool Test1(S1 u) + { + return u is ""a""; + } + + static bool Test2(S1 u) + { + return u is not ""b""; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "FalseFalseTrueFalseTrueTrueFalseFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 27 (0x1b) + .maxstack 2 + .locals init (string V_0) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_0019 + IL_0003: ldarg.0 + IL_0004: ldloca.s V_0 + IL_0006: callvirt ""bool S1.TryGetValue(out string)"" + IL_000b: brfalse.s IL_0019 + IL_000d: ldloc.0 + IL_000e: ldstr ""a"" + IL_0013: call ""bool string.op_Equality(string, string)"" + IL_0018: ret + IL_0019: ldc.i4.0 + IL_001a: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 34 (0x22) + .maxstack 2 + .locals init (string V_0, + bool V_1) + IL_0000: ldarg.0 + IL_0001: brfalse.s IL_001e + IL_0003: ldarg.0 + IL_0004: ldloca.s V_0 + IL_0006: callvirt ""bool S1.TryGetValue(out string)"" + IL_000b: brfalse.s IL_001a + IL_000d: ldloc.0 + IL_000e: ldstr ""b"" + IL_0013: call ""bool string.op_Equality(string, string)"" + IL_0018: brtrue.s IL_001e + IL_001a: ldc.i4.1 + IL_001b: stloc.1 + IL_001c: br.s IL_0020 + IL_001e: ldc.i4.0 + IL_001f: stloc.1 + IL_0020: ldloc.1 + IL_0021: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_23_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool TryGetValue(out int x) { if (_value is int v) { x = v; return true; } x = 0; return false; } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(Test1((new S1(""a""), 1))); + } + + static bool Test1((S1, int) u) + { + return u is (int, 1) or (int, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseFalse").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 39 (0x27) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brfalse.s IL_0023 + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: stloc.2 + IL_0019: ldloc.2 + IL_001a: ldc.i4.1 + IL_001b: sub + IL_001c: ldc.i4.1 + IL_001d: bgt.un.s IL_0023 + IL_001f: ldc.i4.1 + IL_0020: stloc.3 + IL_0021: br.s IL_0025 + IL_0023: ldc.i4.0 + IL_0024: stloc.3 + IL_0025: ldloc.3 + IL_0026: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_24_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + } + + static bool Test1((S1, int) u) + { + return u is (null, 2) or (int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "get_Value TryGetValue True; get_Value True; get_Value False; get_Value TryGetValue False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 55 (0x37) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""object S1.Value.get"" + IL_000e: brtrue.s IL_001b + IL_0010: ldarg.0 + IL_0011: ldfld ""int System.ValueTuple.Item2"" + IL_0016: ldc.i4.2 + IL_0017: beq.s IL_002f + IL_0019: br.s IL_0033 + IL_001b: ldloca.s V_0 + IL_001d: ldloca.s V_1 + IL_001f: call ""bool S1.TryGetValue(out int)"" + IL_0024: brfalse.s IL_0033 + IL_0026: ldarg.0 + IL_0027: ldfld ""int System.ValueTuple.Item2"" + IL_002c: ldc.i4.1 + IL_002d: bne.un.s IL_0033 + IL_002f: ldc.i4.1 + IL_0030: stloc.2 + IL_0031: br.s IL_0035 + IL_0033: ldc.i4.0 + IL_0034: stloc.2 + IL_0035: ldloc.2 + IL_0036: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_25_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + } + + static bool Test1((S1, int) u) + { + return u is (int, 1) or (null, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TryGetValue True; TryGetValue get_Value True; TryGetValue get_Value False; TryGetValue get_Value False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 55 (0x37) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brfalse.s IL_001d + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: ldc.i4.1 + IL_0019: beq.s IL_002f + IL_001b: br.s IL_0033 + IL_001d: ldloca.s V_0 + IL_001f: call ""object S1.Value.get"" + IL_0024: brtrue.s IL_0033 + IL_0026: ldarg.0 + IL_0027: ldfld ""int System.ValueTuple.Item2"" + IL_002c: ldc.i4.2 + IL_002d: bne.un.s IL_0033 + IL_002f: ldc.i4.1 + IL_0030: stloc.2 + IL_0031: br.s IL_0035 + IL_0033: ldc.i4.0 + IL_0034: stloc.2 + IL_0035: ldloc.2 + IL_0036: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_26_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), -1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (not null, 2) or (int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "get_Value TryGetValue True; get_Value TryGetValue False; get_Value False; get_Value False; get_Value TryGetValue False; get_Value True").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 50 (0x32) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""object S1.Value.get"" + IL_000e: brfalse.s IL_002e + IL_0010: ldarg.0 + IL_0011: ldfld ""int System.ValueTuple.Item2"" + IL_0016: stloc.1 + IL_0017: ldloc.1 + IL_0018: ldc.i4.2 + IL_0019: beq.s IL_002a + IL_001b: ldloca.s V_0 + IL_001d: ldloca.s V_2 + IL_001f: call ""bool S1.TryGetValue(out int)"" + IL_0024: brfalse.s IL_002e + IL_0026: ldloc.1 + IL_0027: ldc.i4.1 + IL_0028: bne.un.s IL_002e + IL_002a: ldc.i4.1 + IL_002b: stloc.3 + IL_002c: br.s IL_0030 + IL_002e: ldc.i4.0 + IL_002f: stloc.3 + IL_0030: ldloc.3 + IL_0031: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_27_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), -1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (int, 1) or (not null, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TryGetValue True; TryGetValue False; TryGetValue get_Value False; TryGetValue get_Value False; TryGetValue get_Value False; TryGetValue get_Value True").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 59 (0x3b) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brfalse.s IL_001f + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: stloc.2 + IL_0019: ldloc.2 + IL_001a: ldc.i4.1 + IL_001b: beq.s IL_0033 + IL_001d: br.s IL_002f + IL_001f: ldloca.s V_0 + IL_0021: call ""object S1.Value.get"" + IL_0026: brfalse.s IL_0037 + IL_0028: ldarg.0 + IL_0029: ldfld ""int System.ValueTuple.Item2"" + IL_002e: stloc.2 + IL_002f: ldloc.2 + IL_0030: ldc.i4.2 + IL_0031: bne.un.s IL_0037 + IL_0033: ldc.i4.1 + IL_0034: stloc.3 + IL_0035: br.s IL_0039 + IL_0037: ldc.i4.0 + IL_0038: stloc.3 + IL_0039: ldloc.3 + IL_003a: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_28_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (null, 2) or (not int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "get_Value TryGetValue False; get_Value True; get_Value False; get_Value True; get_Value TryGetValue True; get_Value TryGetValue False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 59 (0x3b) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""object S1.Value.get"" + IL_000e: brtrue.s IL_001d + IL_0010: ldarg.0 + IL_0011: ldfld ""int System.ValueTuple.Item2"" + IL_0016: stloc.1 + IL_0017: ldloc.1 + IL_0018: ldc.i4.2 + IL_0019: beq.s IL_0033 + IL_001b: br.s IL_002f + IL_001d: ldloca.s V_0 + IL_001f: ldloca.s V_2 + IL_0021: call ""bool S1.TryGetValue(out int)"" + IL_0026: brtrue.s IL_0037 + IL_0028: ldarg.0 + IL_0029: ldfld ""int System.ValueTuple.Item2"" + IL_002e: stloc.1 + IL_002f: ldloc.1 + IL_0030: ldc.i4.1 + IL_0031: bne.un.s IL_0037 + IL_0033: ldc.i4.1 + IL_0034: stloc.3 + IL_0035: br.s IL_0039 + IL_0037: ldc.i4.0 + IL_0038: stloc.3 + IL_0039: ldloc.3 + IL_003a: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_29_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (not int, 1) or (null, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TryGetValue False; TryGetValue get_Value True; TryGetValue get_Value False; TryGetValue True; TryGetValue True; TryGetValue get_Value False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 50 (0x32) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brtrue.s IL_002e + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: stloc.2 + IL_0019: ldloc.2 + IL_001a: ldc.i4.1 + IL_001b: beq.s IL_002a + IL_001d: ldloca.s V_0 + IL_001f: call ""object S1.Value.get"" + IL_0024: brtrue.s IL_002e + IL_0026: ldloc.2 + IL_0027: ldc.i4.2 + IL_0028: bne.un.s IL_002e + IL_002a: ldc.i4.1 + IL_002b: stloc.3 + IL_002c: br.s IL_0030 + IL_002e: ldc.i4.0 + IL_002f: stloc.3 + IL_0030: ldloc.3 + IL_0031: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_30_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + } + + static bool Test1((S1, int) u) + { + return u is (null, 2) or (int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "HasValue TryGetValue True; HasValue True; HasValue False; HasValue TryGetValue False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 55 (0x37) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""bool S1.HasValue.get"" + IL_000e: brtrue.s IL_001b + IL_0010: ldarg.0 + IL_0011: ldfld ""int System.ValueTuple.Item2"" + IL_0016: ldc.i4.2 + IL_0017: beq.s IL_002f + IL_0019: br.s IL_0033 + IL_001b: ldloca.s V_0 + IL_001d: ldloca.s V_1 + IL_001f: call ""bool S1.TryGetValue(out int)"" + IL_0024: brfalse.s IL_0033 + IL_0026: ldarg.0 + IL_0027: ldfld ""int System.ValueTuple.Item2"" + IL_002c: ldc.i4.1 + IL_002d: bne.un.s IL_0033 + IL_002f: ldc.i4.1 + IL_0030: stloc.2 + IL_0031: br.s IL_0035 + IL_0033: ldc.i4.0 + IL_0034: stloc.2 + IL_0035: ldloc.2 + IL_0036: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_31_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + } + + static bool Test1((S1, int) u) + { + return u is (int, 1) or (null, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TryGetValue True; TryGetValue HasValue True; TryGetValue HasValue False; TryGetValue HasValue False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 55 (0x37) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brfalse.s IL_001d + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: ldc.i4.1 + IL_0019: beq.s IL_002f + IL_001b: br.s IL_0033 + IL_001d: ldloca.s V_0 + IL_001f: call ""bool S1.HasValue.get"" + IL_0024: brtrue.s IL_0033 + IL_0026: ldarg.0 + IL_0027: ldfld ""int System.ValueTuple.Item2"" + IL_002c: ldc.i4.2 + IL_002d: bne.un.s IL_0033 + IL_002f: ldc.i4.1 + IL_0030: stloc.2 + IL_0031: br.s IL_0035 + IL_0033: ldc.i4.0 + IL_0034: stloc.2 + IL_0035: ldloc.2 + IL_0036: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_32_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), -1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (not null, 2) or (int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "HasValue TryGetValue True; HasValue TryGetValue False; HasValue False; HasValue False; HasValue TryGetValue False; HasValue True").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 50 (0x32) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""bool S1.HasValue.get"" + IL_000e: brfalse.s IL_002e + IL_0010: ldarg.0 + IL_0011: ldfld ""int System.ValueTuple.Item2"" + IL_0016: stloc.1 + IL_0017: ldloc.1 + IL_0018: ldc.i4.2 + IL_0019: beq.s IL_002a + IL_001b: ldloca.s V_0 + IL_001d: ldloca.s V_2 + IL_001f: call ""bool S1.TryGetValue(out int)"" + IL_0024: brfalse.s IL_002e + IL_0026: ldloc.1 + IL_0027: ldc.i4.1 + IL_0028: bne.un.s IL_002e + IL_002a: ldc.i4.1 + IL_002b: stloc.3 + IL_002c: br.s IL_0030 + IL_002e: ldc.i4.0 + IL_002f: stloc.3 + IL_0030: ldloc.3 + IL_0031: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_33_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), -1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (int, 1) or (not null, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TryGetValue True; TryGetValue False; TryGetValue HasValue False; TryGetValue HasValue False; TryGetValue HasValue False; TryGetValue HasValue True").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 59 (0x3b) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brfalse.s IL_001f + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: stloc.2 + IL_0019: ldloc.2 + IL_001a: ldc.i4.1 + IL_001b: beq.s IL_0033 + IL_001d: br.s IL_002f + IL_001f: ldloca.s V_0 + IL_0021: call ""bool S1.HasValue.get"" + IL_0026: brfalse.s IL_0037 + IL_0028: ldarg.0 + IL_0029: ldfld ""int System.ValueTuple.Item2"" + IL_002e: stloc.2 + IL_002f: ldloc.2 + IL_0030: ldc.i4.2 + IL_0031: bne.un.s IL_0037 + IL_0033: ldc.i4.1 + IL_0034: stloc.3 + IL_0035: br.s IL_0039 + IL_0037: ldc.i4.0 + IL_0038: stloc.3 + IL_0039: ldloc.3 + IL_003a: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_34_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (null, 2) or (not int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "HasValue TryGetValue False; HasValue True; HasValue False; HasValue True; HasValue TryGetValue True; HasValue TryGetValue False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 59 (0x3b) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""bool S1.HasValue.get"" + IL_000e: brtrue.s IL_001d + IL_0010: ldarg.0 + IL_0011: ldfld ""int System.ValueTuple.Item2"" + IL_0016: stloc.1 + IL_0017: ldloc.1 + IL_0018: ldc.i4.2 + IL_0019: beq.s IL_0033 + IL_001b: br.s IL_002f + IL_001d: ldloca.s V_0 + IL_001f: ldloca.s V_2 + IL_0021: call ""bool S1.TryGetValue(out int)"" + IL_0026: brtrue.s IL_0037 + IL_0028: ldarg.0 + IL_0029: ldfld ""int System.ValueTuple.Item2"" + IL_002e: stloc.1 + IL_002f: ldloc.1 + IL_0030: ldc.i4.1 + IL_0031: bne.un.s IL_0037 + IL_0033: ldc.i4.1 + IL_0034: stloc.3 + IL_0035: br.s IL_0039 + IL_0037: ldc.i4.0 + IL_0038: stloc.3 + IL_0039: ldloc.3 + IL_003a: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_35_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (not int, 1) or (null, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TryGetValue False; TryGetValue HasValue True; TryGetValue HasValue False; TryGetValue True; TryGetValue True; TryGetValue HasValue False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 50 (0x32) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brtrue.s IL_002e + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: stloc.2 + IL_0019: ldloc.2 + IL_001a: ldc.i4.1 + IL_001b: beq.s IL_002a + IL_001d: ldloca.s V_0 + IL_001f: call ""bool S1.HasValue.get"" + IL_0024: brtrue.s IL_002e + IL_0026: ldloc.2 + IL_0027: ldc.i4.2 + IL_0028: bne.un.s IL_002e + IL_002a: ldc.i4.1 + IL_002b: stloc.3 + IL_002c: br.s IL_0030 + IL_002e: ldc.i4.0 + IL_002f: stloc.3 + IL_0030: ldloc.3 + IL_0031: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_36_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + } + + static bool Test1((S1, int) u) + { + return u is (null, 2) or (int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "HasValue get_Value True; HasValue True; HasValue False; HasValue get_Value False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 58 (0x3a) + .maxstack 2 + .locals init (S1 V_0, + bool V_1) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""bool S1.HasValue.get"" + IL_000e: brtrue.s IL_001b + IL_0010: ldarg.0 + IL_0011: ldfld ""int System.ValueTuple.Item2"" + IL_0016: ldc.i4.2 + IL_0017: beq.s IL_0032 + IL_0019: br.s IL_0036 + IL_001b: ldloca.s V_0 + IL_001d: call ""object S1.Value.get"" + IL_0022: isinst ""int"" + IL_0027: brfalse.s IL_0036 + IL_0029: ldarg.0 + IL_002a: ldfld ""int System.ValueTuple.Item2"" + IL_002f: ldc.i4.1 + IL_0030: bne.un.s IL_0036 + IL_0032: ldc.i4.1 + IL_0033: stloc.1 + IL_0034: br.s IL_0038 + IL_0036: ldc.i4.0 + IL_0037: stloc.1 + IL_0038: ldloc.1 + IL_0039: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_37_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + } + + static bool Test1((S1, int) u) + { + return u is (int, 1) or (null, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "get_Value True; get_Value HasValue True; get_Value HasValue False; get_Value HasValue False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 58 (0x3a) + .maxstack 2 + .locals init (S1 V_0, + bool V_1) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""object S1.Value.get"" + IL_000e: isinst ""int"" + IL_0013: brfalse.s IL_0020 + IL_0015: ldarg.0 + IL_0016: ldfld ""int System.ValueTuple.Item2"" + IL_001b: ldc.i4.1 + IL_001c: beq.s IL_0032 + IL_001e: br.s IL_0036 + IL_0020: ldloca.s V_0 + IL_0022: call ""bool S1.HasValue.get"" + IL_0027: brtrue.s IL_0036 + IL_0029: ldarg.0 + IL_002a: ldfld ""int System.ValueTuple.Item2"" + IL_002f: ldc.i4.2 + IL_0030: bne.un.s IL_0036 + IL_0032: ldc.i4.1 + IL_0033: stloc.1 + IL_0034: br.s IL_0038 + IL_0036: ldc.i4.0 + IL_0037: stloc.1 + IL_0038: ldloc.1 + IL_0039: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_38_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), -1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (not null, 2) or (int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "HasValue get_Value True; HasValue get_Value False; HasValue False; HasValue False; HasValue get_Value False; HasValue True").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 53 (0x35) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""bool S1.HasValue.get"" + IL_000e: brfalse.s IL_0031 + IL_0010: ldarg.0 + IL_0011: ldfld ""int System.ValueTuple.Item2"" + IL_0016: stloc.1 + IL_0017: ldloc.1 + IL_0018: ldc.i4.2 + IL_0019: beq.s IL_002d + IL_001b: ldloca.s V_0 + IL_001d: call ""object S1.Value.get"" + IL_0022: isinst ""int"" + IL_0027: brfalse.s IL_0031 + IL_0029: ldloc.1 + IL_002a: ldc.i4.1 + IL_002b: bne.un.s IL_0031 + IL_002d: ldc.i4.1 + IL_002e: stloc.2 + IL_002f: br.s IL_0033 + IL_0031: ldc.i4.0 + IL_0032: stloc.2 + IL_0033: ldloc.2 + IL_0034: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_39_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), -1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (int, 1) or (not null, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "get_Value True; get_Value False; get_Value HasValue False; get_Value HasValue False; get_Value HasValue False; get_Value HasValue True").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 62 (0x3e) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""object S1.Value.get"" + IL_000e: isinst ""int"" + IL_0013: brfalse.s IL_0022 + IL_0015: ldarg.0 + IL_0016: ldfld ""int System.ValueTuple.Item2"" + IL_001b: stloc.1 + IL_001c: ldloc.1 + IL_001d: ldc.i4.1 + IL_001e: beq.s IL_0036 + IL_0020: br.s IL_0032 + IL_0022: ldloca.s V_0 + IL_0024: call ""bool S1.HasValue.get"" + IL_0029: brfalse.s IL_003a + IL_002b: ldarg.0 + IL_002c: ldfld ""int System.ValueTuple.Item2"" + IL_0031: stloc.1 + IL_0032: ldloc.1 + IL_0033: ldc.i4.2 + IL_0034: bne.un.s IL_003a + IL_0036: ldc.i4.1 + IL_0037: stloc.2 + IL_0038: br.s IL_003c + IL_003a: ldc.i4.0 + IL_003b: stloc.2 + IL_003c: ldloc.2 + IL_003d: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_40_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (null, 2) or (not int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "HasValue get_Value False; HasValue True; HasValue False; HasValue True; HasValue get_Value True; HasValue get_Value False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 62 (0x3e) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""bool S1.HasValue.get"" + IL_000e: brtrue.s IL_001d + IL_0010: ldarg.0 + IL_0011: ldfld ""int System.ValueTuple.Item2"" + IL_0016: stloc.1 + IL_0017: ldloc.1 + IL_0018: ldc.i4.2 + IL_0019: beq.s IL_0036 + IL_001b: br.s IL_0032 + IL_001d: ldloca.s V_0 + IL_001f: call ""object S1.Value.get"" + IL_0024: isinst ""int"" + IL_0029: brtrue.s IL_003a + IL_002b: ldarg.0 + IL_002c: ldfld ""int System.ValueTuple.Item2"" + IL_0031: stloc.1 + IL_0032: ldloc.1 + IL_0033: ldc.i4.1 + IL_0034: bne.un.s IL_003a + IL_0036: ldc.i4.1 + IL_0037: stloc.2 + IL_0038: br.s IL_003c + IL_003a: ldc.i4.0 + IL_003b: stloc.2 + IL_003c: ldloc.2 + IL_003d: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_41_HasValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + } + + static bool Test1((S1, int) u) + { + return u is (not int, 1) or (null, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "get_Value False; get_Value HasValue True; get_Value HasValue False; get_Value True; get_Value True; get_Value HasValue False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 53 (0x35) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""object S1.Value.get"" + IL_000e: isinst ""int"" + IL_0013: brtrue.s IL_0031 + IL_0015: ldarg.0 + IL_0016: ldfld ""int System.ValueTuple.Item2"" + IL_001b: stloc.1 + IL_001c: ldloc.1 + IL_001d: ldc.i4.1 + IL_001e: beq.s IL_002d + IL_0020: ldloca.s V_0 + IL_0022: call ""bool S1.HasValue.get"" + IL_0027: brtrue.s IL_0031 + IL_0029: ldloc.1 + IL_002a: ldc.i4.2 + IL_002b: bne.un.s IL_0031 + IL_002d: ldc.i4.1 + IL_002e: stloc.2 + IL_002f: br.s IL_0033 + IL_0031: ldc.i4.0 + IL_0032: stloc.2 + IL_0033: ldloc.2 + IL_0034: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_42_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue(int) ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + public bool TryGetValue(out string x) + { + System.Console.Write(""TryGetValue(string) ""); + x = _value as string; + return x != null; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 3))); + } + + static bool Test1((S1, int) u) + { + return u is (string, 2) or (int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TryGetValue(string) TryGetValue(int) True; TryGetValue(string) TryGetValue(int) False; TryGetValue(string) TryGetValue(int) False; TryGetValue(string) TryGetValue(int) False; TryGetValue(string) TryGetValue(int) False; TryGetValue(string) TryGetValue(int) False; TryGetValue(string) False; TryGetValue(string) True; TryGetValue(string) False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 57 (0x39) + .maxstack 2 + .locals init (S1 V_0, + string V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out string)"" + IL_0010: brfalse.s IL_001d + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: ldc.i4.2 + IL_0019: beq.s IL_0031 + IL_001b: br.s IL_0035 + IL_001d: ldloca.s V_0 + IL_001f: ldloca.s V_2 + IL_0021: call ""bool S1.TryGetValue(out int)"" + IL_0026: brfalse.s IL_0035 + IL_0028: ldarg.0 + IL_0029: ldfld ""int System.ValueTuple.Item2"" + IL_002e: ldc.i4.1 + IL_002f: bne.un.s IL_0035 + IL_0031: ldc.i4.1 + IL_0032: stloc.3 + IL_0033: br.s IL_0037 + IL_0035: ldc.i4.0 + IL_0036: stloc.3 + IL_0037: ldloc.3 + IL_0038: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_43_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out string x) + { + System.Console.Write(""TryGetValue(string) ""); + x = _value as string; + return x != null; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 3))); + } + + static bool Test1((S1, int) u) + { + return u is (string, 2) or (int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TryGetValue(string) get_Value True; TryGetValue(string) get_Value False; TryGetValue(string) get_Value False; TryGetValue(string) get_Value False; TryGetValue(string) get_Value False; TryGetValue(string) get_Value False; TryGetValue(string) False; TryGetValue(string) True; TryGetValue(string) False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 60 (0x3c) + .maxstack 2 + .locals init (S1 V_0, + string V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out string)"" + IL_0010: brfalse.s IL_001d + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: ldc.i4.2 + IL_0019: beq.s IL_0034 + IL_001b: br.s IL_0038 + IL_001d: ldloca.s V_0 + IL_001f: call ""object S1.Value.get"" + IL_0024: isinst ""int"" + IL_0029: brfalse.s IL_0038 + IL_002b: ldarg.0 + IL_002c: ldfld ""int System.ValueTuple.Item2"" + IL_0031: ldc.i4.1 + IL_0032: bne.un.s IL_0038 + IL_0034: ldc.i4.1 + IL_0035: stloc.2 + IL_0036: br.s IL_003a + IL_0038: ldc.i4.0 + IL_0039: stloc.2 + IL_003a: ldloc.2 + IL_003b: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_44_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue(int) ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 3))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 3))); + } + + static bool Test1((S1, int) u) + { + return u is (string, 2) or (int, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "get_Value TryGetValue(int) True; get_Value TryGetValue(int) False; get_Value TryGetValue(int) False; get_Value TryGetValue(int) False; get_Value TryGetValue(int) False; get_Value TryGetValue(int) False; get_Value False; get_Value True; get_Value False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 60 (0x3c) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + bool V_2) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""object S1.Value.get"" + IL_000e: isinst ""string"" + IL_0013: brfalse.s IL_0020 + IL_0015: ldarg.0 + IL_0016: ldfld ""int System.ValueTuple.Item2"" + IL_001b: ldc.i4.2 + IL_001c: beq.s IL_0034 + IL_001e: br.s IL_0038 + IL_0020: ldloca.s V_0 + IL_0022: ldloca.s V_1 + IL_0024: call ""bool S1.TryGetValue(out int)"" + IL_0029: brfalse.s IL_0038 + IL_002b: ldarg.0 + IL_002c: ldfld ""int System.ValueTuple.Item2"" + IL_0031: ldc.i4.1 + IL_0032: bne.un.s IL_0038 + IL_0034: ldc.i4.1 + IL_0035: stloc.2 + IL_0036: br.s IL_003a + IL_0038: ldc.i4.0 + IL_0039: stloc.2 + IL_003a: ldloc.2 + IL_003b: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_45_TryGetValue() + { + var src = @" +interface I1; + +class C11; +class C12; +class C13 : C12, I1; +class C14 : I1; +class C15 : I1; + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(I1 x) { _value = x; } + public S1(C11 x) { _value = x; } + public S1(C12 x) { _value = x; } + public S1(C14 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out I1 x) + { + System.Console.Write(""TryGetValue(I1) ""); + x = _value as I1; + return x != null; + } + + public bool TryGetValue(out C12 x) + { + System.Console.Write(""TryGetValue(C12) ""); + x = _value as C12; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1((C12)new C13()), new S1(new C14()), new S1(new C15())]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (C12 and I1, 2) or (I1, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: TryGetValue(C12): (Item1, ReturnItem) t2 = t1; [2] +[2]: t2.ReturnItem == True ? [3] : [7] +[3]: t3 = (C12)t2.Item1; [4] +[4]: t3 is I1 ? [5] : [12] +[5]: t4 = t0.Item2; [6] +[6]: t4 == 2 ? [11] : [10] +[7]: TryGetValue(I1): (Item1, ReturnItem) t5 = t1; [8] +[8]: t5.ReturnItem == True ? [9] : [12] +[9]: t4 = t0.Item2; [10] +[10]: t4 == 1 ? [11] : [12] +[11]: leaf `(C12 and I1, 2) or (I1, 1)` +[12]: leaf `u is (C12 and I1, 2) or (I1, 1)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) +False +TryGetValue(C12) +False +TryGetValue(C12) +False +TryGetValue(C12) +True +TryGetValue(C12) +True +TryGetValue(C12) +False +TryGetValue(C12) TryGetValue(I1) +True +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +True +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 72 (0x48) + .maxstack 2 + .locals init (S1 V_0, + C12 V_1, + int V_2, + I1 V_3, + bool V_4) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out C12)"" + IL_0010: brfalse.s IL_0027 + IL_0012: ldloc.1 + IL_0013: isinst ""I1"" + IL_0018: brfalse.s IL_0042 + IL_001a: ldarg.0 + IL_001b: ldfld ""int System.ValueTuple.Item2"" + IL_0020: stloc.2 + IL_0021: ldloc.2 + IL_0022: ldc.i4.2 + IL_0023: beq.s IL_003d + IL_0025: br.s IL_0039 + IL_0027: ldloca.s V_0 + IL_0029: ldloca.s V_3 + IL_002b: call ""bool S1.TryGetValue(out I1)"" + IL_0030: brfalse.s IL_0042 + IL_0032: ldarg.0 + IL_0033: ldfld ""int System.ValueTuple.Item2"" + IL_0038: stloc.2 + IL_0039: ldloc.2 + IL_003a: ldc.i4.1 + IL_003b: bne.un.s IL_0042 + IL_003d: ldc.i4.1 + IL_003e: stloc.s V_4 + IL_0040: br.s IL_0045 + IL_0042: ldc.i4.0 + IL_0043: stloc.s V_4 + IL_0045: ldloc.s V_4 + IL_0047: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_46_TryGetValue() + { + var src = @" +interface I1; + +class C11; +class C12; +class C13 : C12, I1; +class C14 : I1; +class C15 : I1; + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(I1 x) { _value = x; } + public S1(C11 x) { _value = x; } + public S1(C12 x) { _value = x; } + public S1(C14 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out I1 x) + { + System.Console.Write(""TryGetValue(I1) ""); + x = _value as I1; + return x != null; + } + + public bool TryGetValue(out C12 x) + { + System.Console.Write(""TryGetValue(C12) ""); + x = _value as C12; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1((C12)new C13()), new S1(new C14()), new S1(new C15())]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (I1, 1) or (C12 and I1, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: TryGetValue(I1): (Item1, ReturnItem) t2 = t1; [2] +[2]: t2.ReturnItem == True ? [3] : [9] +[3]: t3 = t0.Item2; [4] +[4]: t3 == 1 ? [8] : [5] +[5]: TryGetValue(C12): (Item1, ReturnItem) t4 = t1; [6] +[6]: t4.ReturnItem == True ? [7] : [9] +[7]: t3 == 2 ? [8] : [9] +[8]: leaf `(I1, 1) or (C12 and I1, 2)` +[9]: leaf `u is (I1, 1) or (C12 and I1, 2)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +True +TryGetValue(I1) TryGetValue(C12) +True +TryGetValue(I1) TryGetValue(C12) +False +TryGetValue(I1) +True +TryGetValue(I1) TryGetValue(C12) +False +TryGetValue(I1) TryGetValue(C12) +False +TryGetValue(I1) +True +TryGetValue(I1) TryGetValue(C12) +False +TryGetValue(I1) TryGetValue(C12) +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 55 (0x37) + .maxstack 2 + .locals init (S1 V_0, + I1 V_1, + int V_2, + C12 V_3, + bool V_4) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out I1)"" + IL_0010: brfalse.s IL_0031 + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: stloc.2 + IL_0019: ldloc.2 + IL_001a: ldc.i4.1 + IL_001b: beq.s IL_002c + IL_001d: ldloca.s V_0 + IL_001f: ldloca.s V_3 + IL_0021: call ""bool S1.TryGetValue(out C12)"" + IL_0026: brfalse.s IL_0031 + IL_0028: ldloc.2 + IL_0029: ldc.i4.2 + IL_002a: bne.un.s IL_0031 + IL_002c: ldc.i4.1 + IL_002d: stloc.s V_4 + IL_002f: br.s IL_0034 + IL_0031: ldc.i4.0 + IL_0032: stloc.s V_4 + IL_0034: ldloc.s V_4 + IL_0036: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_47_TryGetValue() + { + var src = @" +interface I1; + +class C11; +class C12; +class C13 : C12, I1; +class C14 : I1; +class C15 : I1; + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(I1 x) { _value = x; } + public S1(C11 x) { _value = x; } + public S1(C12 x) { _value = x; } + public S1(C14 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out I1 x) + { + System.Console.Write(""TryGetValue(I1) ""); + x = _value as I1; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1((C12)new C13()), new S1(new C14()), new S1(new C15())]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (C12 and I1, 2) or (I1, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: t2 = t1.Value; [2] +[2]: t2 is C12 ? [3] : [7] +[3]: t3 = (C12)t2; [4] +[4]: t3 is I1 ? [5] : [12] +[5]: t4 = t0.Item2; [6] +[6]: t4 == 2 ? [11] : [10] +[7]: TryGetValue(I1): (Item1, ReturnItem) t5 = t1; [8] +[8]: t5.ReturnItem == True ? [9] : [12] +[9]: t4 = t0.Item2; [10] +[10]: t4 == 1 ? [11] : [12] +[11]: leaf `(C12 and I1, 2) or (I1, 1)` +[12]: leaf `u is (C12 and I1, 2) or (I1, 1)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +get_Value TryGetValue(I1) +False +get_Value TryGetValue(I1) +False +get_Value TryGetValue(I1) +False +get_Value TryGetValue(I1) +False +get_Value TryGetValue(I1) +False +get_Value TryGetValue(I1) +False +get_Value +False +get_Value +False +get_Value +False +get_Value +True +get_Value +True +get_Value +False +get_Value TryGetValue(I1) +True +get_Value TryGetValue(I1) +False +get_Value TryGetValue(I1) +False +get_Value TryGetValue(I1) +True +get_Value TryGetValue(I1) +False +get_Value TryGetValue(I1) +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 77 (0x4d) + .maxstack 2 + .locals init (S1 V_0, + C12 V_1, + int V_2, + I1 V_3, + bool V_4) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""object S1.Value.get"" + IL_000e: isinst ""C12"" + IL_0013: stloc.1 + IL_0014: ldloc.1 + IL_0015: brfalse.s IL_002c + IL_0017: ldloc.1 + IL_0018: isinst ""I1"" + IL_001d: brfalse.s IL_0047 + IL_001f: ldarg.0 + IL_0020: ldfld ""int System.ValueTuple.Item2"" + IL_0025: stloc.2 + IL_0026: ldloc.2 + IL_0027: ldc.i4.2 + IL_0028: beq.s IL_0042 + IL_002a: br.s IL_003e + IL_002c: ldloca.s V_0 + IL_002e: ldloca.s V_3 + IL_0030: call ""bool S1.TryGetValue(out I1)"" + IL_0035: brfalse.s IL_0047 + IL_0037: ldarg.0 + IL_0038: ldfld ""int System.ValueTuple.Item2"" + IL_003d: stloc.2 + IL_003e: ldloc.2 + IL_003f: ldc.i4.1 + IL_0040: bne.un.s IL_0047 + IL_0042: ldc.i4.1 + IL_0043: stloc.s V_4 + IL_0045: br.s IL_004a + IL_0047: ldc.i4.0 + IL_0048: stloc.s V_4 + IL_004a: ldloc.s V_4 + IL_004c: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_48_TryGetValue() + { + var src = @" +interface I1; + +class C11; +class C12; +class C13 : C12, I1; +class C14 : I1; +class C15 : I1; + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(I1 x) { _value = x; } + public S1(C11 x) { _value = x; } + public S1(C12 x) { _value = x; } + public S1(C14 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out I1 x) + { + System.Console.Write(""TryGetValue(I1) ""); + x = _value as I1; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1((C12)new C13()), new S1(new C14()), new S1(new C15())]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (I1, 1) or (C12 and I1, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +False +TryGetValue(I1) +True +TryGetValue(I1) get_Value +True +TryGetValue(I1) get_Value +False +TryGetValue(I1) +True +TryGetValue(I1) get_Value +False +TryGetValue(I1) get_Value +False +TryGetValue(I1) +True +TryGetValue(I1) get_Value +False +TryGetValue(I1) get_Value +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 55 (0x37) + .maxstack 2 + .locals init (S1 V_0, + I1 V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out I1)"" + IL_0010: brfalse.s IL_0033 + IL_0012: ldarg.0 + IL_0013: ldfld ""int System.ValueTuple.Item2"" + IL_0018: stloc.2 + IL_0019: ldloc.2 + IL_001a: ldc.i4.1 + IL_001b: beq.s IL_002f + IL_001d: ldloca.s V_0 + IL_001f: call ""object S1.Value.get"" + IL_0024: isinst ""C12"" + IL_0029: brfalse.s IL_0033 + IL_002b: ldloc.2 + IL_002c: ldc.i4.2 + IL_002d: bne.un.s IL_0033 + IL_002f: ldc.i4.1 + IL_0030: stloc.3 + IL_0031: br.s IL_0035 + IL_0033: ldc.i4.0 + IL_0034: stloc.3 + IL_0035: ldloc.3 + IL_0036: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_49_TryGetValue() + { + var src = @" +interface I1; + +class C11; +class C12; +class C13 : C12, I1; +class C14 : I1; +class C15 : I1; + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(I1 x) { _value = x; } + public S1(C11 x) { _value = x; } + public S1(C12 x) { _value = x; } + public S1(C14 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out C12 x) + { + System.Console.Write(""TryGetValue(C12) ""); + x = _value as C12; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1((C12)new C13()), new S1(new C14()), new S1(new C15())]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (C12 and I1, 2) or (I1, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: TryGetValue(C12): (Item1, ReturnItem) t2 = t1; [2] +[2]: t2.ReturnItem == True ? [3] : [7] +[3]: t3 = (C12)t2.Item1; [4] +[4]: t3 is I1 ? [5] : [12] +[5]: t4 = t0.Item2; [6] +[6]: t4 == 2 ? [11] : [10] +[7]: t5 = t1.Value; [8] +[8]: t5 is I1 ? [9] : [12] +[9]: t4 = t0.Item2; [10] +[10]: t4 == 1 ? [11] : [12] +[11]: leaf `(C12 and I1, 2) or (I1, 1)` +[12]: leaf `u is (C12 and I1, 2) or (I1, 1)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(C12) get_Value +False +TryGetValue(C12) get_Value +False +TryGetValue(C12) get_Value +False +TryGetValue(C12) get_Value +False +TryGetValue(C12) get_Value +False +TryGetValue(C12) get_Value +False +TryGetValue(C12) +False +TryGetValue(C12) +False +TryGetValue(C12) +False +TryGetValue(C12) +True +TryGetValue(C12) +True +TryGetValue(C12) +False +TryGetValue(C12) get_Value +True +TryGetValue(C12) get_Value +False +TryGetValue(C12) get_Value +False +TryGetValue(C12) get_Value +True +TryGetValue(C12) get_Value +False +TryGetValue(C12) get_Value +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 72 (0x48) + .maxstack 2 + .locals init (S1 V_0, + C12 V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out C12)"" + IL_0010: brfalse.s IL_0027 + IL_0012: ldloc.1 + IL_0013: isinst ""I1"" + IL_0018: brfalse.s IL_0044 + IL_001a: ldarg.0 + IL_001b: ldfld ""int System.ValueTuple.Item2"" + IL_0020: stloc.2 + IL_0021: ldloc.2 + IL_0022: ldc.i4.2 + IL_0023: beq.s IL_0040 + IL_0025: br.s IL_003c + IL_0027: ldloca.s V_0 + IL_0029: call ""object S1.Value.get"" + IL_002e: isinst ""I1"" + IL_0033: brfalse.s IL_0044 + IL_0035: ldarg.0 + IL_0036: ldfld ""int System.ValueTuple.Item2"" + IL_003b: stloc.2 + IL_003c: ldloc.2 + IL_003d: ldc.i4.1 + IL_003e: bne.un.s IL_0044 + IL_0040: ldc.i4.1 + IL_0041: stloc.3 + IL_0042: br.s IL_0046 + IL_0044: ldc.i4.0 + IL_0045: stloc.3 + IL_0046: ldloc.3 + IL_0047: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_50_TryGetValue() + { + var src = @" +interface I1; + +class C11; +class C12; +class C13 : C12, I1; +class C14 : I1; +class C15 : I1; + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(I1 x) { _value = x; } + public S1(C11 x) { _value = x; } + public S1(C12 x) { _value = x; } + public S1(C14 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out C12 x) + { + System.Console.Write(""TryGetValue(C12) ""); + x = _value as C12; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1((C12)new C13()), new S1(new C14()), new S1(new C15())]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (I1, 1) or (C12 and I1, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: t2 = t1.Value; [2] +[2]: t2 is I1 ? [3] : [9] +[3]: t3 = t0.Item2; [4] +[4]: t3 == 1 ? [8] : [5] +[5]: TryGetValue(C12): (Item1, ReturnItem) t4 = t1; [6] +[6]: t4.ReturnItem == True ? [7] : [9] +[7]: t3 == 2 ? [8] : [9] +[8]: leaf `(I1, 1) or (C12 and I1, 2)` +[9]: leaf `u is (I1, 1) or (C12 and I1, 2)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +get_Value +False +get_Value +False +get_Value +False +get_Value +False +get_Value +False +get_Value +False +get_Value +False +get_Value +False +get_Value +False +get_Value +True +get_Value TryGetValue(C12) +True +get_Value TryGetValue(C12) +False +get_Value +True +get_Value TryGetValue(C12) +False +get_Value TryGetValue(C12) +False +get_Value +True +get_Value TryGetValue(C12) +False +get_Value TryGetValue(C12) +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 55 (0x37) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + C12 V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: call ""object S1.Value.get"" + IL_000e: isinst ""I1"" + IL_0013: brfalse.s IL_0033 + IL_0015: ldarg.0 + IL_0016: ldfld ""int System.ValueTuple.Item2"" + IL_001b: stloc.1 + IL_001c: ldloc.1 + IL_001d: ldc.i4.1 + IL_001e: beq.s IL_002f + IL_0020: ldloca.s V_0 + IL_0022: ldloca.s V_2 + IL_0024: call ""bool S1.TryGetValue(out C12)"" + IL_0029: brfalse.s IL_0033 + IL_002b: ldloc.1 + IL_002c: ldc.i4.2 + IL_002d: bne.un.s IL_0033 + IL_002f: ldc.i4.1 + IL_0030: stloc.3 + IL_0031: br.s IL_0035 + IL_0033: ldc.i4.0 + IL_0034: stloc.3 + IL_0035: ldloc.3 + IL_0036: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_51_TryGetValue() + { + var src = @" +interface I1 +{ + int F {get;} +} + +class C11; +class C12; +class C13(int f) : C12, I1 +{ + public int F => f; +} +class C14(int f) : I1 +{ + public int F => f; +} +class C15(int f) : I1 +{ + public int F => f; +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(I1 x) { _value = x; } + public S1(C11 x) { _value = x; } + public S1(C12 x) { _value = x; } + public S1(C14 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out I1 x) + { + System.Console.Write(""TryGetValue(I1) ""); + x = _value as I1; + return x != null; + } + + public bool TryGetValue(out C12 x) + { + System.Console.Write(""TryGetValue(C12) ""); + x = _value as C12; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1((C12)new C13(1)), new S1(new C14(1)), new S1(new C15(1)), new S1((C12)new C13(2)), new S1(new C14(2)), new S1(new C15(2))]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (C12 and I1 and { F: 1 }, 2) or (I1 and { F: 1 }, 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: TryGetValue(C12): (Item1, ReturnItem) t2 = t1; [2] +[2]: t2.ReturnItem == True ? [3] : [10] +[3]: t3 = (C12)t2.Item1; [4] +[4]: t3 is I1 ? [5] : [18] +[5]: t4 = (I1)t3; [6] +[6]: t5 = t4.F; [7] +[7]: t5 == 1 ? [8] : [18] +[8]: t6 = t0.Item2; [9] +[9]: t6 == 2 ? [17] : [16] +[10]: TryGetValue(I1): (Item1, ReturnItem) t7 = t1; [11] +[11]: t7.ReturnItem == True ? [12] : [18] +[12]: t4 = (I1)t7.Item1; [13] +[13]: t5 = t4.F; [14] +[14]: t5 == 1 ? [15] : [18] +[15]: t6 = t0.Item2; [16] +[16]: t6 == 1 ? [17] : [18] +[17]: leaf `(C12 and I1 and { F: 1 }, 2) or (I1 and { F: 1 }, 1)` +[18]: leaf `u is (C12 and I1 and { F: 1 }, 2) or (I1 and { F: 1 }, 1)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) +False +TryGetValue(C12) +False +TryGetValue(C12) +False +TryGetValue(C12) +True +TryGetValue(C12) +True +TryGetValue(C12) +False +TryGetValue(C12) TryGetValue(I1) +True +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +True +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) +False +TryGetValue(C12) +False +TryGetValue(C12) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +TryGetValue(C12) TryGetValue(I1) +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 95 (0x5f) + .maxstack 2 + .locals init (S1 V_0, + C12 V_1, + I1 V_2, + int V_3, + I1 V_4, + bool V_5) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out C12)"" + IL_0010: brfalse.s IL_0032 + IL_0012: ldloc.1 + IL_0013: isinst ""I1"" + IL_0018: stloc.2 + IL_0019: ldloc.2 + IL_001a: brfalse.s IL_0059 + IL_001c: ldloc.2 + IL_001d: callvirt ""int I1.F.get"" + IL_0022: ldc.i4.1 + IL_0023: bne.un.s IL_0059 + IL_0025: ldarg.0 + IL_0026: ldfld ""int System.ValueTuple.Item2"" + IL_002b: stloc.3 + IL_002c: ldloc.3 + IL_002d: ldc.i4.2 + IL_002e: beq.s IL_0054 + IL_0030: br.s IL_0050 + IL_0032: ldloca.s V_0 + IL_0034: ldloca.s V_4 + IL_0036: call ""bool S1.TryGetValue(out I1)"" + IL_003b: brfalse.s IL_0059 + IL_003d: ldloc.s V_4 + IL_003f: stloc.2 + IL_0040: ldloc.2 + IL_0041: callvirt ""int I1.F.get"" + IL_0046: ldc.i4.1 + IL_0047: bne.un.s IL_0059 + IL_0049: ldarg.0 + IL_004a: ldfld ""int System.ValueTuple.Item2"" + IL_004f: stloc.3 + IL_0050: ldloc.3 + IL_0051: ldc.i4.1 + IL_0052: bne.un.s IL_0059 + IL_0054: ldc.i4.1 + IL_0055: stloc.s V_5 + IL_0057: br.s IL_005c + IL_0059: ldc.i4.0 + IL_005a: stloc.s V_5 + IL_005c: ldloc.s V_5 + IL_005e: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_52_TryGetValue() + { + var src = @" +using System; + +class C11; + +class C12 : IComparable +{ + public int CompareTo(object obj) => throw null; +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C11 x) { _value = x; } + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(IComparable x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue(int) ""); + if (_value is int) + { + x = (int)_value; + return true; + } + + x = 0; + return false; + } + + public bool TryGetValue(out IComparable x) + { + System.Console.Write(""TryGetValue(IComparable) ""); + x = _value as IComparable; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1(1), new S1(""1""), new S1(2), new S1(""2""), new S1(3), new S1(""3"")]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (System.IComparable and int and 1, 2) or (int and (1 or 3), 1); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: TryGetValue(System.IComparable): (Item1, ReturnItem) t2 = t1; [2] +[2]: t2.ReturnItem == True ? [3] : [13] +[3]: t3 = (System.IComparable)t2.Item1; [4] +[4]: t3 is int ? [5] : [13] +[5]: t4 = (int)t3; [6] +[6]: t4 == 1 ? [7] : [9] +[7]: t5 = t0.Item2; [8] +[8]: t5 == 2 ? [12] : [11] +[9]: t4 == 3 ? [10] : [13] +[10]: t5 = t0.Item2; [11] +[11]: t5 == 1 ? [12] : [13] +[12]: leaf `(System.IComparable and int and 1, 2) or (int and (1 or 3), 1)` +[13]: leaf `u is (System.IComparable and int and 1, 2) or (int and (1 or 3), 1)` +", +forLowering: true); + + CompilationVerifier verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +True +TryGetValue(IComparable) +True +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +True +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +TryGetValue(IComparable) +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 84 (0x54) + .maxstack 2 + .locals init (S1 V_0, + System.IComparable V_1, + System.IComparable V_2, + int V_3, + int V_4, + bool V_5) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out System.IComparable)"" + IL_0010: brfalse.s IL_004e + IL_0012: ldloc.1 + IL_0013: stloc.2 + IL_0014: ldloc.2 + IL_0015: isinst ""int"" + IL_001a: brfalse.s IL_004e + IL_001c: ldloc.2 + IL_001d: unbox.any ""int"" + IL_0022: stloc.3 + IL_0023: ldloc.3 + IL_0024: ldc.i4.1 + IL_0025: beq.s IL_002d + IL_0027: ldloc.3 + IL_0028: ldc.i4.3 + IL_0029: beq.s IL_003c + IL_002b: br.s IL_004e + IL_002d: ldarg.0 + IL_002e: ldfld ""int System.ValueTuple.Item2"" + IL_0033: stloc.s V_4 + IL_0035: ldloc.s V_4 + IL_0037: ldc.i4.2 + IL_0038: beq.s IL_0049 + IL_003a: br.s IL_0044 + IL_003c: ldarg.0 + IL_003d: ldfld ""int System.ValueTuple.Item2"" + IL_0042: stloc.s V_4 + IL_0044: ldloc.s V_4 + IL_0046: ldc.i4.1 + IL_0047: bne.un.s IL_004e + IL_0049: ldc.i4.1 + IL_004a: stloc.s V_5 + IL_004c: br.s IL_0051 + IL_004e: ldc.i4.0 + IL_004f: stloc.s V_5 + IL_0051: ldloc.s V_5 + IL_0053: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_53_TryGetValue() + { + var src = @" +using System; + +class C11; + +class C12; + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C11 x) { _value = x; } + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(IComparable x) { _value = x; } + public S1(C12 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue(int) ""); + if (_value is int) + { + x = (int)_value; + return true; + } + + x = 0; + return false; + } + + public bool TryGetValue(out IComparable x) + { + System.Console.Write(""TryGetValue(IComparable) ""); + x = _value as IComparable; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1(1), new S1(""1""), new S1(2), new S1(""2""), new S1(3), new S1(""3"")]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (int and 1, 2) or (System.IComparable and { AsInt: 3 }, 1); + } +} + +static class IComparableExtensions +{ + extension(IComparable c) + { + public int? AsInt + { + get + { + c.GetHashCode(); // We do not expect null inputs + var result = c as int?; + + if (result.HasValue && result.Value == 0) + { + throw new Exception(""Unexpected 0 value""); + } + + return result; + } + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: TryGetValue(int): (Item1, ReturnItem) t2 = t1; [2] +[2]: t2.ReturnItem == True ? [3] : [15] +[3]: t3 = (int)t2.Item1; [4] +[4]: t3 == 1 ? [5] : [13] +[5]: t4 = t0.Item2; [6] +[6]: t4 == 2 ? [24] : [7] +[7]: t5 = (System.IComparable)t2.Item1; [8] +[8]: PassThrough t5; [9] +[9]: t7 = t5.AsInt; [10] +[10]: t7 != null ? [11] : [25] +[11]: t8 = (int)t7; [12] +[12]: t8 == 3 ? [23] : [25] +[13]: t5 = (System.IComparable)t2.Item1; [14] +[14]: PassThrough t5; [18] +[15]: TryGetValue(System.IComparable): (Item1, ReturnItem) t9 = t1; [16] +[16]: t9.ReturnItem == True ? [17] : [25] +[17]: t5 = (System.IComparable)t9.Item1; [18] +[18]: t7 = t5.AsInt; [19] +[19]: t7 != null ? [20] : [25] +[20]: t8 = (int)t7; [21] +[21]: t8 == 3 ? [22] : [25] +[22]: t4 = t0.Item2; [23] +[23]: t4 == 1 ? [24] : [25] +[24]: leaf `(int and 1, 2) or (System.IComparable and { AsInt: 3 }, 1)` +[25]: leaf `u is (int and 1, 2) or (System.IComparable and { AsInt: 3 }, 1)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) +False +TryGetValue(int) +True +TryGetValue(int) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) +False +TryGetValue(int) +False +TryGetValue(int) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) +True +TryGetValue(int) +False +TryGetValue(int) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(IComparable) +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 141 (0x8d) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + System.IComparable V_3, + int? V_4, + System.IComparable V_5, + bool V_6) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brfalse.s IL_004e + IL_0012: ldloc.1 + IL_0013: ldc.i4.1 + IL_0014: bne.un.s IL_0045 + IL_0016: ldarg.0 + IL_0017: ldfld ""int System.ValueTuple.Item2"" + IL_001c: stloc.2 + IL_001d: ldloc.2 + IL_001e: ldc.i4.2 + IL_001f: beq.s IL_0082 + IL_0021: ldloc.1 + IL_0022: box ""int"" + IL_0027: stloc.3 + IL_0028: ldloc.3 + IL_0029: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_002e: stloc.s V_4 + IL_0030: ldloca.s V_4 + IL_0032: call ""bool int?.HasValue.get"" + IL_0037: brfalse.s IL_0087 + IL_0039: ldloca.s V_4 + IL_003b: call ""int int?.GetValueOrDefault()"" + IL_0040: ldc.i4.3 + IL_0041: beq.s IL_007e + IL_0043: br.s IL_0087 + IL_0045: ldloc.1 + IL_0046: box ""int"" + IL_004b: stloc.3 + IL_004c: br.s IL_005c + IL_004e: ldloca.s V_0 + IL_0050: ldloca.s V_5 + IL_0052: call ""bool S1.TryGetValue(out System.IComparable)"" + IL_0057: brfalse.s IL_0087 + IL_0059: ldloc.s V_5 + IL_005b: stloc.3 + IL_005c: ldloc.3 + IL_005d: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_0062: stloc.s V_4 + IL_0064: ldloca.s V_4 + IL_0066: call ""bool int?.HasValue.get"" + IL_006b: brfalse.s IL_0087 + IL_006d: ldloca.s V_4 + IL_006f: call ""int int?.GetValueOrDefault()"" + IL_0074: ldc.i4.3 + IL_0075: bne.un.s IL_0087 + IL_0077: ldarg.0 + IL_0078: ldfld ""int System.ValueTuple.Item2"" + IL_007d: stloc.2 + IL_007e: ldloc.2 + IL_007f: ldc.i4.1 + IL_0080: bne.un.s IL_0087 + IL_0082: ldc.i4.1 + IL_0083: stloc.s V_6 + IL_0085: br.s IL_008a + IL_0087: ldc.i4.0 + IL_0088: stloc.s V_6 + IL_008a: ldloc.s V_6 + IL_008c: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_54_TryGetValue() + { + var src = @" +using System; + +class C11; + +class C12 : IConvertible +{ + public TypeCode GetTypeCode() => throw null; + public bool ToBoolean(IFormatProvider provider) => throw null; + public byte ToByte(IFormatProvider provider) => throw null; + public char ToChar(IFormatProvider provider) => throw null; + public DateTime ToDateTime(IFormatProvider provider) => throw null; + public decimal ToDecimal(IFormatProvider provider) => throw null; + public double ToDouble(IFormatProvider provider) => throw null; + public short ToInt16(IFormatProvider provider) => throw null; + public int ToInt32(IFormatProvider provider) => throw null; + public long ToInt64(IFormatProvider provider) => throw null; + public sbyte ToSByte(IFormatProvider provider) => throw null; + public float ToSingle(IFormatProvider provider) => throw null; + public string ToString(IFormatProvider provider) => throw null; + public object ToType(Type conversionType, IFormatProvider provider) => throw null; + public ushort ToUInt16(IFormatProvider provider) => throw null; + public uint ToUInt32(IFormatProvider provider) => throw null; + public ulong ToUInt64(IFormatProvider provider) => throw null; +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C11 x) { _value = x; } + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(IComparable x) { _value = x; } + public S1(IConvertible x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue(int) ""); + if (_value is int) + { + x = (int)_value; + return true; + } + + x = 0; + return false; + } + + public bool TryGetValue(out IComparable x) + { + System.Console.Write(""TryGetValue(IComparable) ""); + x = _value as IComparable; + return x != null; + } + + public bool TryGetValue(out IConvertible x) + { + System.Console.Write(""TryGetValue(IConvertible) ""); + x = _value as IConvertible; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1(1), new S1(""1""), new S1(2), new S1(""2""), new S1(3), new S1(""3"")]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (IConvertible and int and 1, 2) or (System.IComparable and { AsInt: 3 }, 1); + } +} + +static class IComparableExtensions +{ + extension(IComparable c) + { + public int? AsInt + { + get + { + c.GetHashCode(); // We do not expect null inputs + var result = c as int?; + + if (result.HasValue && result.Value == 0) + { + throw new Exception(""Unexpected 0 value""); + } + + return result; + } + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: TryGetValue(System.IConvertible): (Item1, ReturnItem) t2 = t1; [2] +[2]: t2.ReturnItem == True ? [3] : [17] +[3]: t3 = (System.IConvertible)t2.Item1; [4] +[4]: t3 is int ? [5] : [17] +[5]: t4 = (int)t3; [6] +[6]: t4 == 1 ? [7] : [15] +[7]: t5 = t0.Item2; [8] +[8]: t5 == 2 ? [26] : [9] +[9]: t6 = (System.IComparable)t3; [10] +[10]: PassThrough t6; [11] +[11]: t8 = t6.AsInt; [12] +[12]: t8 != null ? [13] : [27] +[13]: t9 = (int)t8; [14] +[14]: t9 == 3 ? [25] : [27] +[15]: t6 = (System.IComparable)t3; [16] +[16]: PassThrough t6; [20] +[17]: TryGetValue(System.IComparable): (Item1, ReturnItem) t10 = t1; [18] +[18]: t10.ReturnItem == True ? [19] : [27] +[19]: t6 = (System.IComparable)t10.Item1; [20] +[20]: t8 = t6.AsInt; [21] +[21]: t8 != null ? [22] : [27] +[22]: t9 = (int)t8; [23] +[23]: t9 == 3 ? [24] : [27] +[24]: t5 = t0.Item2; [25] +[25]: t5 == 1 ? [26] : [27] +[26]: leaf `(IConvertible and int and 1, 2) or (System.IComparable and { AsInt: 3 }, 1)` +[27]: leaf `u is (IConvertible and int and 1, 2) or (System.IComparable and { AsInt: 3 }, 1)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) +True +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) +True +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 161 (0xa1) + .maxstack 2 + .locals init (S1 V_0, + System.IConvertible V_1, + System.IConvertible V_2, + int V_3, + System.IComparable V_4, + int? V_5, + System.IComparable V_6, + bool V_7) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out System.IConvertible)"" + IL_0010: brfalse.s IL_0060 + IL_0012: ldloc.1 + IL_0013: stloc.2 + IL_0014: ldloc.2 + IL_0015: isinst ""int"" + IL_001a: brfalse.s IL_0060 + IL_001c: ldloc.2 + IL_001d: unbox.any ""int"" + IL_0022: ldc.i4.1 + IL_0023: bne.un.s IL_0056 + IL_0025: ldarg.0 + IL_0026: ldfld ""int System.ValueTuple.Item2"" + IL_002b: stloc.3 + IL_002c: ldloc.3 + IL_002d: ldc.i4.2 + IL_002e: beq.s IL_0096 + IL_0030: ldloc.2 + IL_0031: castclass ""System.IComparable"" + IL_0036: stloc.s V_4 + IL_0038: ldloc.s V_4 + IL_003a: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_003f: stloc.s V_5 + IL_0041: ldloca.s V_5 + IL_0043: call ""bool int?.HasValue.get"" + IL_0048: brfalse.s IL_009b + IL_004a: ldloca.s V_5 + IL_004c: call ""int int?.GetValueOrDefault()"" + IL_0051: ldc.i4.3 + IL_0052: beq.s IL_0092 + IL_0054: br.s IL_009b + IL_0056: ldloc.2 + IL_0057: castclass ""System.IComparable"" + IL_005c: stloc.s V_4 + IL_005e: br.s IL_006f + IL_0060: ldloca.s V_0 + IL_0062: ldloca.s V_6 + IL_0064: call ""bool S1.TryGetValue(out System.IComparable)"" + IL_0069: brfalse.s IL_009b + IL_006b: ldloc.s V_6 + IL_006d: stloc.s V_4 + IL_006f: ldloc.s V_4 + IL_0071: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_0076: stloc.s V_5 + IL_0078: ldloca.s V_5 + IL_007a: call ""bool int?.HasValue.get"" + IL_007f: brfalse.s IL_009b + IL_0081: ldloca.s V_5 + IL_0083: call ""int int?.GetValueOrDefault()"" + IL_0088: ldc.i4.3 + IL_0089: bne.un.s IL_009b + IL_008b: ldarg.0 + IL_008c: ldfld ""int System.ValueTuple.Item2"" + IL_0091: stloc.3 + IL_0092: ldloc.3 + IL_0093: ldc.i4.1 + IL_0094: bne.un.s IL_009b + IL_0096: ldc.i4.1 + IL_0097: stloc.s V_7 + IL_0099: br.s IL_009e + IL_009b: ldc.i4.0 + IL_009c: stloc.s V_7 + IL_009e: ldloc.s V_7 + IL_00a0: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_55_TryGetValue() + { + var src = @" +using System; + +class C11; + +class C12 : IConvertible +{ + public TypeCode GetTypeCode() => throw null; + public bool ToBoolean(IFormatProvider provider) => throw null; + public byte ToByte(IFormatProvider provider) => throw null; + public char ToChar(IFormatProvider provider) => throw null; + public DateTime ToDateTime(IFormatProvider provider) => throw null; + public decimal ToDecimal(IFormatProvider provider) => throw null; + public double ToDouble(IFormatProvider provider) => throw null; + public short ToInt16(IFormatProvider provider) => throw null; + public int ToInt32(IFormatProvider provider) => throw null; + public long ToInt64(IFormatProvider provider) => throw null; + public sbyte ToSByte(IFormatProvider provider) => throw null; + public float ToSingle(IFormatProvider provider) => throw null; + public string ToString(IFormatProvider provider) => throw null; + public object ToType(Type conversionType, IFormatProvider provider) => throw null; + public ushort ToUInt16(IFormatProvider provider) => throw null; + public uint ToUInt32(IFormatProvider provider) => throw null; + public ulong ToUInt64(IFormatProvider provider) => throw null; +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C11 x) { _value = x; } + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(IComparable x) { _value = x; } + public S1(IConvertible x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue(int) ""); + if (_value is int) + { + x = (int)_value; + return true; + } + + x = 0; + return false; + } + + public bool TryGetValue(out IComparable x) + { + System.Console.Write(""TryGetValue(IComparable) ""); + x = _value as IComparable; + return x != null; + } + + public bool TryGetValue(out IConvertible x) + { + System.Console.Write(""TryGetValue(IConvertible) ""); + x = _value as IConvertible; + return x != null; + } + + public bool TryGetValue(out string x) + { + System.Console.Write(""TryGetValue(string) ""); + x = _value as string; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1(1), new S1(""1""), new S1(2), new S1(""2""), new S1(3), new S1(""3"")]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (int and 1, 2) or (string and ""3"", 3) or (System.IComparable and { AsInt: 3 }, 1); + } +} + +static class IComparableExtensions +{ + extension(IComparable c) + { + public int? AsInt + { + get + { + c.GetHashCode(); // We do not expect null inputs + var result = c as int?; + + if (result.HasValue && result.Value == 0) + { + throw new Exception(""Unexpected 0 value""); + } + + return result; + } + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: TryGetValue(int): (Item1, ReturnItem) t2 = t1; [2] +[2]: t2.ReturnItem == True ? [3] : [11] +[3]: t3 = (int)t2.Item1; [4] +[4]: t3 == 1 ? [5] : [9] +[5]: t4 = t0.Item2; [6] +[6]: t4 == 2 ? [34] : [7] +[7]: t5 = (System.IComparable)t2.Item1; [8] +[8]: PassThrough t5; [19] +[9]: t5 = (System.IComparable)t2.Item1; [10] +[10]: PassThrough t5; [28] +[11]: TryGetValue(string): (Item1, ReturnItem) t7 = t1; [12] +[12]: t7.ReturnItem == True ? [13] : [25] +[13]: t8 = (string)t7.Item1; [14] +[14]: t8 == ""3"" ? [15] : [23] +[15]: t4 = t0.Item2; [16] +[16]: t4 == 3 ? [34] : [17] +[17]: t5 = (System.IComparable)t7.Item1; [18] +[18]: PassThrough t5; [19] +[19]: t10 = t5.AsInt; [20] +[20]: t10 != null ? [21] : [35] +[21]: t11 = (int)t10; [22] +[22]: t11 == 3 ? [33] : [35] +[23]: t5 = (System.IComparable)t7.Item1; [24] +[24]: PassThrough t5; [28] +[25]: TryGetValue(System.IComparable): (Item1, ReturnItem) t12 = t1; [26] +[26]: t12.ReturnItem == True ? [27] : [35] +[27]: t5 = (System.IComparable)t12.Item1; [28] +[28]: t10 = t5.AsInt; [29] +[29]: t10 != null ? [30] : [35] +[30]: t11 = (int)t10; [31] +[31]: t11 == 3 ? [32] : [35] +[32]: t4 = t0.Item2; [33] +[33]: t4 == 1 ? [34] : [35] +[34]: leaf `(int and 1, 2) or (string and ""3"", 3) or (System.IComparable and { AsInt: 3 }, 1)` +[35]: leaf `u is (int and 1, 2) or (string and ""3"", 3) or (System.IComparable and { AsInt: 3 }, 1)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(int) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(int) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(int) +False +TryGetValue(int) +True +TryGetValue(int) +False +TryGetValue(int) TryGetValue(string) +False +TryGetValue(int) TryGetValue(string) +False +TryGetValue(int) TryGetValue(string) +False +TryGetValue(int) +False +TryGetValue(int) +False +TryGetValue(int) +False +TryGetValue(int) TryGetValue(string) +False +TryGetValue(int) TryGetValue(string) +False +TryGetValue(int) TryGetValue(string) +False +TryGetValue(int) +True +TryGetValue(int) +False +TryGetValue(int) +False +TryGetValue(int) TryGetValue(string) +False +TryGetValue(int) TryGetValue(string) +False +TryGetValue(int) TryGetValue(string) +True +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 190 (0xbe) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + System.IComparable V_3, + string V_4, + int? V_5, + System.IComparable V_6, + bool V_7) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brfalse.s IL_0036 + IL_0012: ldloc.1 + IL_0013: ldc.i4.1 + IL_0014: bne.un.s IL_002d + IL_0016: ldarg.0 + IL_0017: ldfld ""int System.ValueTuple.Item2"" + IL_001c: stloc.2 + IL_001d: ldloc.2 + IL_001e: ldc.i4.2 + IL_001f: beq IL_00b3 + IL_0024: ldloc.1 + IL_0025: box ""int"" + IL_002a: stloc.3 + IL_002b: br.s IL_005d + IL_002d: ldloc.1 + IL_002e: box ""int"" + IL_0033: stloc.3 + IL_0034: br.s IL_008d + IL_0036: ldloca.s V_0 + IL_0038: ldloca.s V_4 + IL_003a: call ""bool S1.TryGetValue(out string)"" + IL_003f: brfalse.s IL_007f + IL_0041: ldloc.s V_4 + IL_0043: ldstr ""3"" + IL_0048: call ""bool string.op_Equality(string, string)"" + IL_004d: brfalse.s IL_007a + IL_004f: ldarg.0 + IL_0050: ldfld ""int System.ValueTuple.Item2"" + IL_0055: stloc.2 + IL_0056: ldloc.2 + IL_0057: ldc.i4.3 + IL_0058: beq.s IL_00b3 + IL_005a: ldloc.s V_4 + IL_005c: stloc.3 + IL_005d: ldloc.3 + IL_005e: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_0063: stloc.s V_5 + IL_0065: ldloca.s V_5 + IL_0067: call ""bool int?.HasValue.get"" + IL_006c: brfalse.s IL_00b8 + IL_006e: ldloca.s V_5 + IL_0070: call ""int int?.GetValueOrDefault()"" + IL_0075: ldc.i4.3 + IL_0076: beq.s IL_00af + IL_0078: br.s IL_00b8 + IL_007a: ldloc.s V_4 + IL_007c: stloc.3 + IL_007d: br.s IL_008d + IL_007f: ldloca.s V_0 + IL_0081: ldloca.s V_6 + IL_0083: call ""bool S1.TryGetValue(out System.IComparable)"" + IL_0088: brfalse.s IL_00b8 + IL_008a: ldloc.s V_6 + IL_008c: stloc.3 + IL_008d: ldloc.3 + IL_008e: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_0093: stloc.s V_5 + IL_0095: ldloca.s V_5 + IL_0097: call ""bool int?.HasValue.get"" + IL_009c: brfalse.s IL_00b8 + IL_009e: ldloca.s V_5 + IL_00a0: call ""int int?.GetValueOrDefault()"" + IL_00a5: ldc.i4.3 + IL_00a6: bne.un.s IL_00b8 + IL_00a8: ldarg.0 + IL_00a9: ldfld ""int System.ValueTuple.Item2"" + IL_00ae: stloc.2 + IL_00af: ldloc.2 + IL_00b0: ldc.i4.1 + IL_00b1: bne.un.s IL_00b8 + IL_00b3: ldc.i4.1 + IL_00b4: stloc.s V_7 + IL_00b6: br.s IL_00bb + IL_00b8: ldc.i4.0 + IL_00b9: stloc.s V_7 + IL_00bb: ldloc.s V_7 + IL_00bd: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_56_TryGetValue() + { + var src = @" +using System; + +class C11; + +class C12 : IConvertible +{ + public TypeCode GetTypeCode() => throw null; + public bool ToBoolean(IFormatProvider provider) => throw null; + public byte ToByte(IFormatProvider provider) => throw null; + public char ToChar(IFormatProvider provider) => throw null; + public DateTime ToDateTime(IFormatProvider provider) => throw null; + public decimal ToDecimal(IFormatProvider provider) => throw null; + public double ToDouble(IFormatProvider provider) => throw null; + public short ToInt16(IFormatProvider provider) => throw null; + public int ToInt32(IFormatProvider provider) => throw null; + public long ToInt64(IFormatProvider provider) => throw null; + public sbyte ToSByte(IFormatProvider provider) => throw null; + public float ToSingle(IFormatProvider provider) => throw null; + public string ToString(IFormatProvider provider) => throw null; + public object ToType(Type conversionType, IFormatProvider provider) => throw null; + public ushort ToUInt16(IFormatProvider provider) => throw null; + public uint ToUInt32(IFormatProvider provider) => throw null; + public ulong ToUInt64(IFormatProvider provider) => throw null; +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C11 x) { _value = x; } + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public S1(IComparable x) { _value = x; } + public S1(IConvertible x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool HasValue + { + get + { + System.Console.Write(""HasValue ""); + return _value != null; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue(int) ""); + if (_value is int) + { + x = (int)_value; + return true; + } + + x = 0; + return false; + } + + public bool TryGetValue(out IComparable x) + { + System.Console.Write(""TryGetValue(IComparable) ""); + x = _value as IComparable; + return x != null; + } + + public bool TryGetValue(out IConvertible x) + { + System.Console.Write(""TryGetValue(IConvertible) ""); + x = _value as IConvertible; + return x != null; + } + + public bool TryGetValue(out string x) + { + System.Console.Write(""TryGetValue(string) ""); + x = _value as string; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), new S1(new C11()), new S1(new C12()), new S1(1), new S1(""1""), new S1(2), new S1(""2""), new S1(3), new S1(""3"")]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((S1, int) u) + { + return u is (IConvertible and int and 1, 2) or (string and ""3"", 3) or (System.IComparable and { AsInt: 3 }, 1); + } +} + +static class IComparableExtensions +{ + extension(IComparable c) + { + public int? AsInt + { + get + { + c.GetHashCode(); // We do not expect null inputs + var result = c as int?; + + if (result.HasValue && result.Value == 0) + { + throw new Exception(""Unexpected 0 value""); + } + + return result; + } + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: TryGetValue(System.IConvertible): (Item1, ReturnItem) t2 = t1; [2] +[2]: t2.ReturnItem == True ? [3] : [27] +[3]: t3 = (System.IConvertible)t2.Item1; [4] +[4]: t3 is int ? [5] : [13] +[5]: t4 = (int)t3; [6] +[6]: t4 == 1 ? [7] : [11] +[7]: t5 = t0.Item2; [8] +[8]: t5 == 2 ? [36] : [9] +[9]: t6 = (System.IComparable)t3; [10] +[10]: PassThrough t6; [21] +[11]: t6 = (System.IComparable)t3; [12] +[12]: PassThrough t6; [30] +[13]: TryGetValue(string): (Item1, ReturnItem) t8 = t1; [14] +[14]: t8.ReturnItem == True ? [15] : [27] +[15]: t9 = (string)t8.Item1; [16] +[16]: t9 == ""3"" ? [17] : [25] +[17]: t5 = t0.Item2; [18] +[18]: t5 == 3 ? [36] : [19] +[19]: t6 = (System.IComparable)t8.Item1; [20] +[20]: PassThrough t6; [21] +[21]: t11 = t6.AsInt; [22] +[22]: t11 != null ? [23] : [37] +[23]: t12 = (int)t11; [24] +[24]: t12 == 3 ? [35] : [37] +[25]: t6 = (System.IComparable)t8.Item1; [26] +[26]: PassThrough t6; [30] +[27]: TryGetValue(System.IComparable): (Item1, ReturnItem) t13 = t1; [28] +[28]: t13.ReturnItem == True ? [29] : [37] +[29]: t6 = (System.IComparable)t13.Item1; [30] +[30]: t11 = t6.AsInt; [31] +[31]: t11 != null ? [32] : [37] +[32]: t12 = (int)t11; [33] +[33]: t12 == 3 ? [34] : [37] +[34]: t5 = t0.Item2; [35] +[35]: t5 == 1 ? [36] : [37] +[36]: leaf `(IConvertible and int and 1, 2) or (string and ""3"", 3) or (System.IComparable and { AsInt: 3 }, 1)` +[37]: leaf `u is (IConvertible and int and 1, 2) or (string and ""3"", 3) or (System.IComparable and { AsInt: 3 }, 1)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(IConvertible) TryGetValue(string) TryGetValue(IComparable) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) +True +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) TryGetValue(string) +False +TryGetValue(IConvertible) TryGetValue(string) +False +TryGetValue(IConvertible) TryGetValue(string) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) TryGetValue(string) +False +TryGetValue(IConvertible) TryGetValue(string) +False +TryGetValue(IConvertible) TryGetValue(string) +False +TryGetValue(IConvertible) +True +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) +False +TryGetValue(IConvertible) TryGetValue(string) +False +TryGetValue(IConvertible) TryGetValue(string) +False +TryGetValue(IConvertible) TryGetValue(string) +True +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 215 (0xd7) + .maxstack 2 + .locals init (S1 V_0, + System.IConvertible V_1, + System.IConvertible V_2, + int V_3, + System.IComparable V_4, + string V_5, + int? V_6, + System.IComparable V_7, + bool V_8) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out System.IConvertible)"" + IL_0010: brfalse IL_0096 + IL_0015: ldloc.1 + IL_0016: stloc.2 + IL_0017: ldloc.2 + IL_0018: isinst ""int"" + IL_001d: brfalse.s IL_004a + IL_001f: ldloc.2 + IL_0020: unbox.any ""int"" + IL_0025: ldc.i4.1 + IL_0026: bne.un.s IL_0040 + IL_0028: ldarg.0 + IL_0029: ldfld ""int System.ValueTuple.Item2"" + IL_002e: stloc.3 + IL_002f: ldloc.3 + IL_0030: ldc.i4.2 + IL_0031: beq IL_00cc + IL_0036: ldloc.2 + IL_0037: castclass ""System.IComparable"" + IL_003c: stloc.s V_4 + IL_003e: br.s IL_0072 + IL_0040: ldloc.2 + IL_0041: castclass ""System.IComparable"" + IL_0046: stloc.s V_4 + IL_0048: br.s IL_00a5 + IL_004a: ldloca.s V_0 + IL_004c: ldloca.s V_5 + IL_004e: call ""bool S1.TryGetValue(out string)"" + IL_0053: brfalse.s IL_0096 + IL_0055: ldloc.s V_5 + IL_0057: ldstr ""3"" + IL_005c: call ""bool string.op_Equality(string, string)"" + IL_0061: brfalse.s IL_0090 + IL_0063: ldarg.0 + IL_0064: ldfld ""int System.ValueTuple.Item2"" + IL_0069: stloc.3 + IL_006a: ldloc.3 + IL_006b: ldc.i4.3 + IL_006c: beq.s IL_00cc + IL_006e: ldloc.s V_5 + IL_0070: stloc.s V_4 + IL_0072: ldloc.s V_4 + IL_0074: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_0079: stloc.s V_6 + IL_007b: ldloca.s V_6 + IL_007d: call ""bool int?.HasValue.get"" + IL_0082: brfalse.s IL_00d1 + IL_0084: ldloca.s V_6 + IL_0086: call ""int int?.GetValueOrDefault()"" + IL_008b: ldc.i4.3 + IL_008c: beq.s IL_00c8 + IL_008e: br.s IL_00d1 + IL_0090: ldloc.s V_5 + IL_0092: stloc.s V_4 + IL_0094: br.s IL_00a5 + IL_0096: ldloca.s V_0 + IL_0098: ldloca.s V_7 + IL_009a: call ""bool S1.TryGetValue(out System.IComparable)"" + IL_009f: brfalse.s IL_00d1 + IL_00a1: ldloc.s V_7 + IL_00a3: stloc.s V_4 + IL_00a5: ldloc.s V_4 + IL_00a7: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_00ac: stloc.s V_6 + IL_00ae: ldloca.s V_6 + IL_00b0: call ""bool int?.HasValue.get"" + IL_00b5: brfalse.s IL_00d1 + IL_00b7: ldloca.s V_6 + IL_00b9: call ""int int?.GetValueOrDefault()"" + IL_00be: ldc.i4.3 + IL_00bf: bne.un.s IL_00d1 + IL_00c1: ldarg.0 + IL_00c2: ldfld ""int System.ValueTuple.Item2"" + IL_00c7: stloc.3 + IL_00c8: ldloc.3 + IL_00c9: ldc.i4.1 + IL_00ca: bne.un.s IL_00d1 + IL_00cc: ldc.i4.1 + IL_00cd: stloc.s V_8 + IL_00cf: br.s IL_00d4 + IL_00d1: ldc.i4.0 + IL_00d2: stloc.s V_8 + IL_00d4: ldloc.s V_8 + IL_00d6: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_57_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue ""); + if (_value is int v) + { + x = v; + return true; + } + + x = 0; + return false; + } + + static void Main() + { + System.Console.Write(Test1((new S1(1), 1))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(1), 2))); + System.Console.Write(""; ""); + System.Console.Write(Test1((new S1(""a""), 1))); + } + + static bool Test1((S1, int) u) + { + return u is (1, 1) or (1, 2); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TryGetValue True; TryGetValue False; TryGetValue True; TryGetValue False").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 43 (0x2b) + .maxstack 2 + .locals init (S1 V_0, + int V_1, + int V_2, + bool V_3) + IL_0000: ldarg.0 + IL_0001: ldfld ""S1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloca.s V_0 + IL_0009: ldloca.s V_1 + IL_000b: call ""bool S1.TryGetValue(out int)"" + IL_0010: brfalse.s IL_0027 + IL_0012: ldloc.1 + IL_0013: ldc.i4.1 + IL_0014: bne.un.s IL_0027 + IL_0016: ldarg.0 + IL_0017: ldfld ""int System.ValueTuple.Item2"" + IL_001c: stloc.2 + IL_001d: ldloc.2 + IL_001e: ldc.i4.1 + IL_001f: sub + IL_0020: ldc.i4.1 + IL_0021: bgt.un.s IL_0027 + IL_0023: ldc.i4.1 + IL_0024: stloc.3 + IL_0025: br.s IL_0029 + IL_0027: ldc.i4.0 + IL_0028: stloc.3 + IL_0029: ldloc.3 + IL_002a: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_58_TryGetValue() + { + var src = @" +class C1(int f1, int f2) +{ + public int F11 = f1; + public int F12 = f2; +} + +class C2(int f11, int f12, int f2) : C1(f11, f12) +{ + public int F2 = f2; +} + +class C3(int f1, int f2) : C1(f1, f2) +{ +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + static void Main() + { + S1[] s = [new S1(), + new S1(new C3(1, 1)), new S1(new C3(1, 3)), new S1(new C3(2, 3)), new S1(new C3(2, 4)), + new S1(new C2(1, 1, 1)), new S1(new C2(1, 3, 1)), new S1(new C2(2, 3, 1)), new S1(new C2(2, 4, 1)), + new S1(new C2(1, 1, 2)), new S1(new C2(1, 3, 2)), new S1(new C2(2, 3, 2)), new S1(new C2(2, 4, 2))]; + foreach (var s1 in s) + { + var t = Test1(s1); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + + static bool Test1(S1 u) + { + return u is not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 }); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Value; [1] +[1]: t1 is C1 ? [2] : [12] +[2]: t2 = (C1)t1; [3] +[3]: t3 = t2.F11; [4] +[4]: t3 == 1 ? [9] : [5] +[5]: t1 is C2 ? [6] : [12] +[6]: t4 = (C2)t1; [7] +[7]: t5 = t4.F2; [8] +[8]: t5 == 2 ? [9] : [12] +[9]: t6 = t2.F12; [10] +[10]: t6 == 3 ? [11] : [12] +[11]: leaf `u is not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 })` +[12]: leaf `not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 })` +", +forLowering: true); + + var verifier = CompileAndVerify(comp, expectedOutput: @" +get_Value +True +get_Value +True +get_Value +False +get_Value +True +get_Value +True +get_Value +True +get_Value +False +get_Value +True +get_Value +True +get_Value +True +get_Value +False +get_Value +False +get_Value +True +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 63 (0x3f) + .maxstack 2 + .locals init (object V_0, + C1 V_1, + C2 V_2, + bool V_3) + IL_0000: ldarga.s V_0 + IL_0002: call ""object S1.Value.get"" + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: isinst ""C1"" + IL_000e: stloc.1 + IL_000f: ldloc.1 + IL_0010: brfalse.s IL_0037 + IL_0012: ldloc.1 + IL_0013: ldfld ""int C1.F11"" + IL_0018: ldc.i4.1 + IL_0019: beq.s IL_002e + IL_001b: ldloc.0 + IL_001c: isinst ""C2"" + IL_0021: stloc.2 + IL_0022: ldloc.2 + IL_0023: brfalse.s IL_0037 + IL_0025: ldloc.2 + IL_0026: ldfld ""int C2.F2"" + IL_002b: ldc.i4.2 + IL_002c: bne.un.s IL_0037 + IL_002e: ldloc.1 + IL_002f: ldfld ""int C1.F12"" + IL_0034: ldc.i4.3 + IL_0035: beq.s IL_003b + IL_0037: ldc.i4.1 + IL_0038: stloc.3 + IL_0039: br.s IL_003d + IL_003b: ldc.i4.0 + IL_003c: stloc.3 + IL_003d: ldloc.3 + IL_003e: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_59_TryGetValue() + { + var src = @" +class C1(int f1, int f2) +{ + public int F11 = f1; + public int F12 = f2; +} + +class C2(int f11, int f12, int f2) : C1(f11, f12) +{ + public int F2 = f2; +} + +class C3(int f1, int f2) : C1(f1, f2) +{ +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out C1 x) + { + System.Console.Write(""TryGetValue(C1) ""); + x = _value as C1; + return x != null; + } + + public bool TryGetValue(out C2 x) + { + System.Console.Write(""TryGetValue(C2) ""); + x = _value as C2; + return x != null; + } + + public bool TryGetValue(out C3 x) + { + System.Console.Write(""TryGetValue(C3) ""); + x = _value as C3; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), + new S1(new C3(1, 1)), new S1(new C3(1, 3)), new S1(new C3(2, 3)), new S1(new C3(2, 4)), + new S1(new C2(1, 1, 1)), new S1(new C2(1, 3, 1)), new S1(new C2(2, 3, 1)), new S1(new C2(2, 4, 1)), + new S1(new C2(1, 1, 2)), new S1(new C2(1, 3, 2)), new S1(new C2(2, 3, 2)), new S1(new C2(2, 4, 2))]; + foreach (var s1 in s) + { + var t = Test1(s1); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + + static bool Test1(S1 u) + { + return u is not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 }); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: TryGetValue(C1): (Item1, ReturnItem) t1 = t0; [1] +[1]: t1.ReturnItem == True ? [2] : [13] +[2]: t2 = (C1)t1.Item1; [3] +[3]: t3 = t2.F11; [4] +[4]: t3 == 1 ? [10] : [5] +[5]: TryGetValue(C2): (Item1, ReturnItem) t4 = t0; [6] +[6]: t4.ReturnItem == True ? [7] : [13] +[7]: t5 = (C2)t4.Item1; [8] +[8]: t6 = t5.F2; [9] +[9]: t6 == 2 ? [10] : [13] +[10]: t7 = t2.F12; [11] +[11]: t7 == 3 ? [12] : [13] +[12]: leaf `u is not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 })` +[13]: leaf `not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 })` +", +forLowering: true); + + var verifier = CompileAndVerify(comp, expectedOutput: @" +TryGetValue(C1) +True +TryGetValue(C1) +True +TryGetValue(C1) +False +TryGetValue(C1) TryGetValue(C2) +True +TryGetValue(C1) TryGetValue(C2) +True +TryGetValue(C1) +True +TryGetValue(C1) +False +TryGetValue(C1) TryGetValue(C2) +True +TryGetValue(C1) TryGetValue(C2) +True +TryGetValue(C1) +True +TryGetValue(C1) +False +TryGetValue(C1) TryGetValue(C2) +False +TryGetValue(C1) TryGetValue(C2) +True +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 59 (0x3b) + .maxstack 2 + .locals init (C1 V_0, + C1 V_1, + C2 V_2, + bool V_3) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out C1)"" + IL_0009: brfalse.s IL_0033 + IL_000b: ldloc.0 + IL_000c: stloc.1 + IL_000d: ldloc.1 + IL_000e: ldfld ""int C1.F11"" + IL_0013: ldc.i4.1 + IL_0014: beq.s IL_002a + IL_0016: ldarga.s V_0 + IL_0018: ldloca.s V_2 + IL_001a: call ""bool S1.TryGetValue(out C2)"" + IL_001f: brfalse.s IL_0033 + IL_0021: ldloc.2 + IL_0022: ldfld ""int C2.F2"" + IL_0027: ldc.i4.2 + IL_0028: bne.un.s IL_0033 + IL_002a: ldloc.1 + IL_002b: ldfld ""int C1.F12"" + IL_0030: ldc.i4.3 + IL_0031: beq.s IL_0037 + IL_0033: ldc.i4.1 + IL_0034: stloc.3 + IL_0035: br.s IL_0039 + IL_0037: ldc.i4.0 + IL_0038: stloc.3 + IL_0039: ldloc.3 + IL_003a: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_60_TryGetValue() + { + var src = @" +class C1(int f1, int f2) +{ + public int F11 = f1; + public int F12 = f2; +} + +class C2(int f11, int f12, int f2) : C1(f11, f12) +{ + public int F2 = f2; +} + +class C3(int f1, int f2) : C1(f1, f2) +{ +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out C1 x) + { + System.Console.Write(""TryGetValue(C1) ""); + x = _value as C1; + return x != null; + } + + public bool TryGetValue(out C2 x) + { + System.Console.Write(""TryGetValue(C2) ""); + x = _value as C2; + return x != null; + } + + public bool TryGetValue(out C3 x) + { + System.Console.Write(""TryGetValue(C3) ""); + x = _value as C3; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), + new S1(new C3(1, 1)), new S1(new C3(1, 3)), new S1(new C3(2, 3)), new S1(new C3(2, 4)), + new S1(new C2(1, 1, 1)), new S1(new C2(1, 3, 1)), new S1(new C2(2, 3, 1)), new S1(new C2(2, 4, 1)), + new S1(new C2(1, 1, 2)), new S1(new C2(1, 3, 2)), new S1(new C2(2, 3, 2)), new S1(new C2(2, 4, 2))]; + foreach (var s1 in s) + { + var t = Test1(s1); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + + static bool Test1(S1 u) + { + return u is not ((C2 { F2: 2 } or C1 { F11: 1 }) and C1 { F12: 3 }); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: TryGetValue(C2): (Item1, ReturnItem) t1 = t0; [1] +[1]: t1.ReturnItem == True ? [2] : [11] +[2]: t2 = (C2)t1.Item1; [3] +[3]: t3 = t2.F2; [4] +[4]: t3 == 2 ? [5] : [6] +[5]: t4 = (C1)t1.Item1; [10] +[6]: t4 = (C1)t1.Item1; [7] +[7]: PassThrough t4; [8] +[8]: t6 = t4.F11; [9] +[9]: t6 == 1 ? [10] : [19] +[10]: PassThrough t4; [16] +[11]: TryGetValue(C1): (Item1, ReturnItem) t7 = t0; [12] +[12]: t7.ReturnItem == True ? [13] : [19] +[13]: t4 = (C1)t7.Item1; [14] +[14]: t6 = t4.F11; [15] +[15]: t6 == 1 ? [16] : [19] +[16]: t8 = t4.F12; [17] +[17]: t8 == 3 ? [18] : [19] +[18]: leaf `u is not ((C2 { F2: 2 } or C1 { F11: 1 }) and C1 { F12: 3 })` +[19]: leaf `not ((C2 { F2: 2 } or C1 { F11: 1 }) and C1 { F12: 3 })` +", +forLowering: true); + + var verifier = CompileAndVerify(comp, expectedOutput: @" +TryGetValue(C2) TryGetValue(C1) +True +TryGetValue(C2) TryGetValue(C1) +True +TryGetValue(C2) TryGetValue(C1) +False +TryGetValue(C2) TryGetValue(C1) +True +TryGetValue(C2) TryGetValue(C1) +True +TryGetValue(C2) +True +TryGetValue(C2) +False +TryGetValue(C2) +True +TryGetValue(C2) +True +TryGetValue(C2) +True +TryGetValue(C2) +False +TryGetValue(C2) +False +TryGetValue(C2) +True +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 76 (0x4c) + .maxstack 2 + .locals init (C2 V_0, + C1 V_1, + C1 V_2, + bool V_3) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out C2)"" + IL_0009: brfalse.s IL_0025 + IL_000b: ldloc.0 + IL_000c: ldfld ""int C2.F2"" + IL_0011: ldc.i4.2 + IL_0012: bne.un.s IL_0018 + IL_0014: ldloc.0 + IL_0015: stloc.1 + IL_0016: br.s IL_003b + IL_0018: ldloc.0 + IL_0019: stloc.1 + IL_001a: ldloc.1 + IL_001b: ldfld ""int C1.F11"" + IL_0020: ldc.i4.1 + IL_0021: bne.un.s IL_0044 + IL_0023: br.s IL_003b + IL_0025: ldarga.s V_0 + IL_0027: ldloca.s V_2 + IL_0029: call ""bool S1.TryGetValue(out C1)"" + IL_002e: brfalse.s IL_0044 + IL_0030: ldloc.2 + IL_0031: stloc.1 + IL_0032: ldloc.1 + IL_0033: ldfld ""int C1.F11"" + IL_0038: ldc.i4.1 + IL_0039: bne.un.s IL_0044 + IL_003b: ldloc.1 + IL_003c: ldfld ""int C1.F12"" + IL_0041: ldc.i4.3 + IL_0042: beq.s IL_0048 + IL_0044: ldc.i4.1 + IL_0045: stloc.3 + IL_0046: br.s IL_004a + IL_0048: ldc.i4.0 + IL_0049: stloc.3 + IL_004a: ldloc.3 + IL_004b: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_61_TryGetValue() + { + var src = @" +class C1(int f1, int f2) +{ + public int F11 = f1; + public int F12 = f2; +} + +class C2(int f11, int f12, int f2) : C1(f11, f12) +{ + public int F2 = f2; +} + +class C3(int f1, int f2) : C1(f1, f2) +{ +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out C2 x) + { + System.Console.Write(""TryGetValue(C2) ""); + x = _value as C2; + return x != null; + } + + public bool TryGetValue(out C3 x) + { + System.Console.Write(""TryGetValue(C3) ""); + x = _value as C3; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), + new S1(new C3(1, 1)), new S1(new C3(1, 3)), new S1(new C3(2, 3)), new S1(new C3(2, 4)), + new S1(new C2(1, 1, 1)), new S1(new C2(1, 3, 1)), new S1(new C2(2, 3, 1)), new S1(new C2(2, 4, 1)), + new S1(new C2(1, 1, 2)), new S1(new C2(1, 3, 2)), new S1(new C2(2, 3, 2)), new S1(new C2(2, 4, 2))]; + foreach (var s1 in s) + { + var t = Test1(s1); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + + static bool Test1(S1 u) + { + return u is not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 }); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Value; [1] +[1]: t1 is C1 ? [2] : [13] +[2]: t2 = (C1)t1; [3] +[3]: t3 = t2.F11; [4] +[4]: t3 == 1 ? [10] : [5] +[5]: TryGetValue(C2): (Item1, ReturnItem) t4 = t0; [6] +[6]: t4.ReturnItem == True ? [7] : [13] +[7]: t5 = (C2)t4.Item1; [8] +[8]: t6 = t5.F2; [9] +[9]: t6 == 2 ? [10] : [13] +[10]: t7 = t2.F12; [11] +[11]: t7 == 3 ? [12] : [13] +[12]: leaf `u is not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 })` +[13]: leaf `not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 })` +", +forLowering: true); + + var verifier = CompileAndVerify(comp, expectedOutput: @" +get_Value +True +get_Value +True +get_Value +False +get_Value TryGetValue(C2) +True +get_Value TryGetValue(C2) +True +get_Value +True +get_Value +False +get_Value TryGetValue(C2) +True +get_Value TryGetValue(C2) +True +get_Value +True +get_Value +False +get_Value TryGetValue(C2) +False +get_Value TryGetValue(C2) +True +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 62 (0x3e) + .maxstack 2 + .locals init (C1 V_0, + C2 V_1, + bool V_2) + IL_0000: ldarga.s V_0 + IL_0002: call ""object S1.Value.get"" + IL_0007: isinst ""C1"" + IL_000c: stloc.0 + IL_000d: ldloc.0 + IL_000e: brfalse.s IL_0036 + IL_0010: ldloc.0 + IL_0011: ldfld ""int C1.F11"" + IL_0016: ldc.i4.1 + IL_0017: beq.s IL_002d + IL_0019: ldarga.s V_0 + IL_001b: ldloca.s V_1 + IL_001d: call ""bool S1.TryGetValue(out C2)"" + IL_0022: brfalse.s IL_0036 + IL_0024: ldloc.1 + IL_0025: ldfld ""int C2.F2"" + IL_002a: ldc.i4.2 + IL_002b: bne.un.s IL_0036 + IL_002d: ldloc.0 + IL_002e: ldfld ""int C1.F12"" + IL_0033: ldc.i4.3 + IL_0034: beq.s IL_003a + IL_0036: ldc.i4.1 + IL_0037: stloc.2 + IL_0038: br.s IL_003c + IL_003a: ldc.i4.0 + IL_003b: stloc.2 + IL_003c: ldloc.2 + IL_003d: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_62_TryGetValue() + { + var src = @" +class C1(int f1, int f2) +{ + public int F11 = f1; + public int F12 = f2; +} + +class C2(int f11, int f12, int f2) : C1(f11, f12) +{ + public int F2 = f2; +} + +class C3(int f1, int f2) : C1(f1, f2) +{ +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out C2 x) + { + System.Console.Write(""TryGetValue(C2) ""); + x = _value as C2; + return x != null; + } + + public bool TryGetValue(out C3 x) + { + System.Console.Write(""TryGetValue(C3) ""); + x = _value as C3; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), + new S1(new C3(1, 1)), new S1(new C3(1, 3)), new S1(new C3(2, 3)), new S1(new C3(2, 4)), + new S1(new C2(1, 1, 1)), new S1(new C2(1, 3, 1)), new S1(new C2(2, 3, 1)), new S1(new C2(2, 4, 1)), + new S1(new C2(1, 1, 2)), new S1(new C2(1, 3, 2)), new S1(new C2(2, 3, 2)), new S1(new C2(2, 4, 2))]; + foreach (var s1 in s) + { + var t = Test1(s1); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + + static bool Test1(S1 u) + { + return u is not ((C2 { F2: 2 } or C1 { F11: 1 }) and C1 { F12: 3 }); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: TryGetValue(C2): (Item1, ReturnItem) t1 = t0; [1] +[1]: t1.ReturnItem == True ? [2] : [11] +[2]: t2 = (C2)t1.Item1; [3] +[3]: t3 = t2.F2; [4] +[4]: t3 == 2 ? [5] : [6] +[5]: t4 = (C1)t1.Item1; [10] +[6]: t4 = (C1)t1.Item1; [7] +[7]: PassThrough t4; [8] +[8]: t6 = t4.F11; [9] +[9]: t6 == 1 ? [10] : [19] +[10]: PassThrough t4; [16] +[11]: t7 = t0.Value; [12] +[12]: t7 is C1 ? [13] : [19] +[13]: t4 = (C1)t7; [14] +[14]: t6 = t4.F11; [15] +[15]: t6 == 1 ? [16] : [19] +[16]: t8 = t4.F12; [17] +[17]: t8 == 3 ? [18] : [19] +[18]: leaf `u is not ((C2 { F2: 2 } or C1 { F11: 1 }) and C1 { F12: 3 })` +[19]: leaf `not ((C2 { F2: 2 } or C1 { F11: 1 }) and C1 { F12: 3 })` +", +forLowering: true); + + var verifier = CompileAndVerify(comp, expectedOutput: @" +TryGetValue(C2) get_Value +True +TryGetValue(C2) get_Value +True +TryGetValue(C2) get_Value +False +TryGetValue(C2) get_Value +True +TryGetValue(C2) get_Value +True +TryGetValue(C2) +True +TryGetValue(C2) +False +TryGetValue(C2) +True +TryGetValue(C2) +True +TryGetValue(C2) +True +TryGetValue(C2) +False +TryGetValue(C2) +False +TryGetValue(C2) +True +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 79 (0x4f) + .maxstack 2 + .locals init (C2 V_0, + C1 V_1, + bool V_2) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out C2)"" + IL_0009: brfalse.s IL_0025 + IL_000b: ldloc.0 + IL_000c: ldfld ""int C2.F2"" + IL_0011: ldc.i4.2 + IL_0012: bne.un.s IL_0018 + IL_0014: ldloc.0 + IL_0015: stloc.1 + IL_0016: br.s IL_003e + IL_0018: ldloc.0 + IL_0019: stloc.1 + IL_001a: ldloc.1 + IL_001b: ldfld ""int C1.F11"" + IL_0020: ldc.i4.1 + IL_0021: bne.un.s IL_0047 + IL_0023: br.s IL_003e + IL_0025: ldarga.s V_0 + IL_0027: call ""object S1.Value.get"" + IL_002c: isinst ""C1"" + IL_0031: stloc.1 + IL_0032: ldloc.1 + IL_0033: brfalse.s IL_0047 + IL_0035: ldloc.1 + IL_0036: ldfld ""int C1.F11"" + IL_003b: ldc.i4.1 + IL_003c: bne.un.s IL_0047 + IL_003e: ldloc.1 + IL_003f: ldfld ""int C1.F12"" + IL_0044: ldc.i4.3 + IL_0045: beq.s IL_004b + IL_0047: ldc.i4.1 + IL_0048: stloc.2 + IL_0049: br.s IL_004d + IL_004b: ldc.i4.0 + IL_004c: stloc.2 + IL_004d: ldloc.2 + IL_004e: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_63_TryGetValue() + { + var src = @" +class C1(int f1, int f2) +{ + public int F11 = f1; + public int F12 = f2; +} + +class C2(int f11, int f12, int f2) : C1(f11, f12) +{ + public int F2 = f2; +} + +class C3(int f1, int f2) : C1(f1, f2) +{ +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out C1 x) + { + System.Console.Write(""TryGetValue(C1) ""); + x = _value as C1; + return x != null; + } + + public bool TryGetValue(out C3 x) + { + System.Console.Write(""TryGetValue(C3) ""); + x = _value as C3; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), + new S1(new C3(1, 1)), new S1(new C3(1, 3)), new S1(new C3(2, 3)), new S1(new C3(2, 4)), + new S1(new C2(1, 1, 1)), new S1(new C2(1, 3, 1)), new S1(new C2(2, 3, 1)), new S1(new C2(2, 4, 1)), + new S1(new C2(1, 1, 2)), new S1(new C2(1, 3, 2)), new S1(new C2(2, 3, 2)), new S1(new C2(2, 4, 2))]; + foreach (var s1 in s) + { + var t = Test1(s1); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + + static bool Test1(S1 u) + { + return u is not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 }); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: TryGetValue(C1): (Item1, ReturnItem) t1 = t0; [1] +[1]: t1.ReturnItem == True ? [2] : [13] +[2]: t2 = (C1)t1.Item1; [3] +[3]: t3 = t2.F11; [4] +[4]: t3 == 1 ? [10] : [5] +[5]: t4 = t0.Value; [6] +[6]: t4 is C2 ? [7] : [13] +[7]: t5 = (C2)t4; [8] +[8]: t6 = t5.F2; [9] +[9]: t6 == 2 ? [10] : [13] +[10]: t7 = t2.F12; [11] +[11]: t7 == 3 ? [12] : [13] +[12]: leaf `u is not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 })` +[13]: leaf `not ((C1 { F11: 1 } or C2 { F2: 2 }) and C1 { F12: 3 })` +", +forLowering: true); + + var verifier = CompileAndVerify(comp, expectedOutput: @" +TryGetValue(C1) +True +TryGetValue(C1) +True +TryGetValue(C1) +False +TryGetValue(C1) get_Value +True +TryGetValue(C1) get_Value +True +TryGetValue(C1) +True +TryGetValue(C1) +False +TryGetValue(C1) get_Value +True +TryGetValue(C1) get_Value +True +TryGetValue(C1) +True +TryGetValue(C1) +False +TryGetValue(C1) get_Value +False +TryGetValue(C1) get_Value +True +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 64 (0x40) + .maxstack 2 + .locals init (C1 V_0, + C1 V_1, + C2 V_2, + bool V_3) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out C1)"" + IL_0009: brfalse.s IL_0038 + IL_000b: ldloc.0 + IL_000c: stloc.1 + IL_000d: ldloc.1 + IL_000e: ldfld ""int C1.F11"" + IL_0013: ldc.i4.1 + IL_0014: beq.s IL_002f + IL_0016: ldarga.s V_0 + IL_0018: call ""object S1.Value.get"" + IL_001d: isinst ""C2"" + IL_0022: stloc.2 + IL_0023: ldloc.2 + IL_0024: brfalse.s IL_0038 + IL_0026: ldloc.2 + IL_0027: ldfld ""int C2.F2"" + IL_002c: ldc.i4.2 + IL_002d: bne.un.s IL_0038 + IL_002f: ldloc.1 + IL_0030: ldfld ""int C1.F12"" + IL_0035: ldc.i4.3 + IL_0036: beq.s IL_003c + IL_0038: ldc.i4.1 + IL_0039: stloc.3 + IL_003a: br.s IL_003e + IL_003c: ldc.i4.0 + IL_003d: stloc.3 + IL_003e: ldloc.3 + IL_003f: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_64_TryGetValue() + { + var src = @" +class C1(int f1, int f2) +{ + public int F11 = f1; + public int F12 = f2; +} + +class C2(int f11, int f12, int f2) : C1(f11, f12) +{ + public int F2 = f2; +} + +class C3(int f1, int f2) : C1(f1, f2) +{ +} + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(C1 x) { _value = x; } + public S1(C2 x) { _value = x; } + public S1(C3 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out C1 x) + { + System.Console.Write(""TryGetValue(C1) ""); + x = _value as C1; + return x != null; + } + + public bool TryGetValue(out C3 x) + { + System.Console.Write(""TryGetValue(C3) ""); + x = _value as C3; + return x != null; + } + + static void Main() + { + S1[] s = [new S1(), + new S1(new C3(1, 1)), new S1(new C3(1, 3)), new S1(new C3(2, 3)), new S1(new C3(2, 4)), + new S1(new C2(1, 1, 1)), new S1(new C2(1, 3, 1)), new S1(new C2(2, 3, 1)), new S1(new C2(2, 4, 1)), + new S1(new C2(1, 1, 2)), new S1(new C2(1, 3, 2)), new S1(new C2(2, 3, 2)), new S1(new C2(2, 4, 2))]; + foreach (var s1 in s) + { + var t = Test1(s1); + System.Console.WriteLine(); + System.Console.WriteLine(t); + } + } + + static bool Test1(S1 u) + { + return u is not ((C2 { F2: 2 } or C1 { F11: 1 }) and C1 { F12: 3 }); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Value; [1] +[1]: t1 is C2 ? [2] : [8] +[2]: t2 = (C2)t1; [3] +[3]: t3 = t2.F2; [4] +[4]: t3 == 2 ? [5] : [6] +[5]: t4 = (C1)t1; [13] +[6]: t4 = (C1)t1; [7] +[7]: PassThrough t4; [11] +[8]: TryGetValue(C1): (Item1, ReturnItem) t6 = t0; [9] +[9]: t6.ReturnItem == True ? [10] : [16] +[10]: t4 = (C1)t6.Item1; [11] +[11]: t7 = t4.F11; [12] +[12]: t7 == 1 ? [13] : [16] +[13]: t8 = t4.F12; [14] +[14]: t8 == 3 ? [15] : [16] +[15]: leaf `u is not ((C2 { F2: 2 } or C1 { F11: 1 }) and C1 { F12: 3 })` +[16]: leaf `not ((C2 { F2: 2 } or C1 { F11: 1 }) and C1 { F12: 3 })` +", +forLowering: true); + + var verifier = CompileAndVerify(comp, expectedOutput: @" +get_Value TryGetValue(C1) +True +get_Value TryGetValue(C1) +True +get_Value TryGetValue(C1) +False +get_Value TryGetValue(C1) +True +get_Value TryGetValue(C1) +True +get_Value +True +get_Value +False +get_Value +True +get_Value +True +get_Value +True +get_Value +False +get_Value +False +get_Value +True +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 87 (0x57) + .maxstack 2 + .locals init (object V_0, + C2 V_1, + C1 V_2, + C1 V_3, + bool V_4) + IL_0000: ldarga.s V_0 + IL_0002: call ""object S1.Value.get"" + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: isinst ""C2"" + IL_000e: stloc.1 + IL_000f: ldloc.1 + IL_0010: brfalse.s IL_002d + IL_0012: ldloc.1 + IL_0013: ldfld ""int C2.F2"" + IL_0018: ldc.i4.2 + IL_0019: bne.un.s IL_0024 + IL_001b: ldloc.0 + IL_001c: castclass ""C1"" + IL_0021: stloc.2 + IL_0022: br.s IL_0043 + IL_0024: ldloc.0 + IL_0025: castclass ""C1"" + IL_002a: stloc.2 + IL_002b: br.s IL_003a + IL_002d: ldarga.s V_0 + IL_002f: ldloca.s V_3 + IL_0031: call ""bool S1.TryGetValue(out C1)"" + IL_0036: brfalse.s IL_004c + IL_0038: ldloc.3 + IL_0039: stloc.2 + IL_003a: ldloc.2 + IL_003b: ldfld ""int C1.F11"" + IL_0040: ldc.i4.1 + IL_0041: bne.un.s IL_004c + IL_0043: ldloc.2 + IL_0044: ldfld ""int C1.F12"" + IL_0049: ldc.i4.3 + IL_004a: beq.s IL_0051 + IL_004c: ldc.i4.1 + IL_004d: stloc.s V_4 + IL_004f: br.s IL_0054 + IL_0051: ldc.i4.0 + IL_0052: stloc.s V_4 + IL_0054: ldloc.s V_4 + IL_0056: ret +} +"); + } + + [Fact] + public void NonBoxingUnionMatching_65_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(System.Runtime.CompilerServices.ITuple x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out System.Runtime.CompilerServices.ITuple x) + { + System.Console.Write(""TryGetValue(ITuple) ""); + x = _value as System.Runtime.CompilerServices.ITuple; + return x != null; + } +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(' '); + System.Console.Write(Test1(default)); + System.Console.Write(' '); + System.Console.Write(Test1(new S1(new C()))); + } + + static bool Test1(S1 u) + { + return u is (_, 10); + } +} + +public class C : System.Runtime.CompilerServices.ITuple +{ + int System.Runtime.CompilerServices.ITuple.Length => 2; + object System.Runtime.CompilerServices.ITuple.this[int i] => i * 10; +} + +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify(comp, expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TryGetValue(ITuple) False TryGetValue(ITuple) False TryGetValue(ITuple) True" : null, verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + + [Fact] + public void NonBoxingUnionMatching_66_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public S1(S2 x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out S2 x) + { + System.Console.Write(""TryGetValue(S2) ""); + if (_value is S2 s2) + { + x = s2; + return true; + } + + x = default; + return false; + } + + public bool TryGetValue(out S2 x) + { + System.Console.Write(""TryGetValue(S2) ""); + if (_value is S2 s2) + { + x = s2; + return true; + } + + x = default; + return false; + } + + public bool TryGetValue(out S2 x) + { + System.Console.Write(""TryGetValue(S2) ""); + if (_value is S2 s2) + { + x = s2; + return true; + } + + x = default; + return false; + } +} + +struct S2 +{ + public T Value; + + public void Deconstruct(out T value, out int x) + { + value = Value; + x = 0; + } +} + +class A; +class B; + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(new S2() { Value = 10 }))); + System.Console.Write(' '); + System.Console.Write(Test1(default)); + System.Console.Write(' '); + System.Console.Write(Test1(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(' '); + System.Console.Write(Test1(new S1(new S2() { Value = 0 }))); + System.Console.Write(' '); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(new S2() { Value = 10 }))); + System.Console.Write(' '); + System.Console.Write(Test2(default)); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(new S2() { Value = ""11"" }))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(new S2() { Value = 0 }))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(new S2() { Value = 11 }))); + System.Console.Write(' '); + System.Console.Write(' '); + System.Console.Write(Test3(new S1(new S2() { Value = 11 }))); + System.Console.Write(' '); + System.Console.Write(Test3(default)); + System.Console.Write(' '); + System.Console.Write(Test3(new S1(new S2() { Value = ""11"" }))); + } + + static bool Test1(S1 u) + { + return u is S2 (10, _); + } + + static bool Test2(S1 u) + { + return u is S2 (10 or 11, _); + } + + static bool Test3(S1 u) + { + return u is S2 (""11"", _) and (['1', '1'], _); + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetCoreApp, options: TestOptions.ReleaseExe); + CompileAndVerify( + comp, + expectedOutput: ExecutionConditionUtil.IsMonoOrCoreClr ? "TryGetValue(S2) True TryGetValue(S2) False TryGetValue(S2) False TryGetValue(S2) False TryGetValue(S2) True TryGetValue(S2) False TryGetValue(S2) False TryGetValue(S2) False TryGetValue(S2) True TryGetValue(S2) False TryGetValue(S2) False TryGetValue(S2) True" : null, + verify: Verification.FailsPEVerify).VerifyDiagnostics(); + } + + [Fact] + public void NonBoxingUnionMatching_67_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue(int) ""); + if (_value is int s2) + { + x = s2; + return true; + } + + x = default; + return false; + } +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(' '); + System.Console.Write(Test1(default)); + System.Console.Write(' '); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(' '); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(' '); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(' '); + System.Console.Write(Test2(default)); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(11))); + } + + static bool Test1(S1 u) + { + return u is int x; + } + + static bool Test2(S1 u) + { + return u is int x ? (x == 10 || x == 11) : false; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], targetFramework: TargetFramework.NetLatest, options: TestOptions.ReleaseExe); + CompileAndVerify( + comp, + expectedOutput: "TryGetValue(int) True TryGetValue(int) False TryGetValue(int) False TryGetValue(int) True TryGetValue(int) True TryGetValue(int) False TryGetValue(int) False TryGetValue(int) False TryGetValue(int) True" + ).VerifyDiagnostics(); + } + + [Fact] + public void NonBoxingUnionMatching_68_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int x) { _value = x; } + public S1(string x) { _value = x; } + public object Value + { + get + { + System.Console.Write(""get_Value ""); + return _value; + } + } + + public bool TryGetValue(out int x) + { + System.Console.Write(""TryGetValue(int) ""); + if (_value is int s2) + { + x = s2; + return true; + } + + x = default; + return false; + } +} + +class Program +{ + static void Main() + { + System.Console.Write(Test1(new S1(10))); + System.Console.Write(' '); + System.Console.Write(Test1(default)); + System.Console.Write(' '); + System.Console.Write(Test1(new S1(""11""))); + System.Console.Write(' '); + System.Console.Write(Test1(new S1(0))); + System.Console.Write(' '); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(10))); + System.Console.Write(' '); + System.Console.Write(Test2(default)); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(""11""))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(0))); + System.Console.Write(' '); + System.Console.Write(Test2(new S1(11))); + } + + static bool Test1(S1 u) + { + return u is >=10; + } + + static bool Test2(S1 u) + { + return u is <10 or 11; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + CompileAndVerify( + comp, + expectedOutput: "TryGetValue(int) True TryGetValue(int) False TryGetValue(int) False TryGetValue(int) False TryGetValue(int) False TryGetValue(int) False TryGetValue(int) False TryGetValue(int) True TryGetValue(int) True" + ).VerifyDiagnostics(); + } + + [Fact] + public void NonBoxingUnionMatching_69_TryGetValue_NullableAnalysis() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public bool TryGetValue(out string? x) => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.TryGetValue(out var value)) + { +#line 100 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 200 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (!s.TryGetValue(out var value)) + { +#line 300 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 400 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(200, 13), + // (201,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(201, 19), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(300, 13), + // (301,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 19) + ); + } + + [Fact] + public void NonBoxingUnionMatching_70_TryGetValue_NullableAnalysis() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public bool TryGetValue(out string? x) => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.Value is null) return; + + if (s.TryGetValue(out var value)) + { +#line 100 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 200 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (s.Value is null) return; + + if (!s.TryGetValue(out var value)) + { +#line 300 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 400 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(200, 13), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(300, 13) + ); + } + + [Fact] + public void NonBoxingUnionMatching_71_TryGetValue_NullableAnalysis() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Value))] + public bool TryGetValue([System.Diagnostics.CodeAnalysis.NotNullWhen(true)]out string? x) => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.TryGetValue(out var value)) + { +#line 100 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 200 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (!s.TryGetValue(out var value)) + { +#line 300 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 400 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, MemberNotNullWhenAttributeDefinition, NotNullWhenAttributeDefinition]); + comp.VerifyDiagnostics( + // (200,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(200, 13), + // (201,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(201, 19), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(300, 13), + // (301,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 19) + ); + } + + [Fact] + public void NonBoxingUnionMatching_72_TryGetValue_NullableAnalysis() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + [System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, nameof(Value))] + public bool TryGetValue([System.Diagnostics.CodeAnalysis.NotNullWhen(true)]out string? x) => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.Value is null) return; + + if (s.TryGetValue(out var value)) + { +#line 100 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 200 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (s.Value is null) return; + + if (!s.TryGetValue(out var value)) + { +#line 300 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 400 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, MemberNotNullWhenAttributeDefinition, NotNullWhenAttributeDefinition]); + comp.VerifyDiagnostics( + // (200,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(200, 13), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(300, 13) + ); + } + + [Fact] + public void NonBoxingUnionMatching_73_TryGetValue_NullableAnalysis() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(int? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public bool TryGetValue(out string? x) => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.TryGetValue(out var value)) + { +#line 100 + value.ToString(); + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 200 + value.ToString(); + _ = s switch { int => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (!s.TryGetValue(out var value)) + { +#line 300 + value.ToString(); + _ = s switch { int => 1, bool => 3 }; + } + else + { +#line 400 + value.ToString(); + _ = s switch { int => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(100, 13), + // (101,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(101, 19), + // (200,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(200, 13), + // (201,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(201, 19), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(300, 13), + // (301,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 19), + // (400,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(400, 13), + // (401,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { int => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(401, 19) + ); + } + + [Fact] + public void NonBoxingUnionMatching_74_TryGetValue_NullableAnalysis_Generic() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(T x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public bool TryGetValue(out T x) => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.TryGetValue(out var value)) + { +#line 100 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 200 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (!s.TryGetValue(out var value)) + { +#line 300 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 400 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (200,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(200, 13), + // (201,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(201, 19), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(300, 13), + // (301,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 19) + ); + } + + [Fact] + public void NonBoxingUnionMatching_75_TryGetValue_NullableAnalysis_Generic() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(T x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public bool TryGetValue(out string? x) => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.TryGetValue(out var value)) + { +#line 100 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 200 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (!s.TryGetValue(out var value)) + { +#line 300 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 400 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(100, 13), + // (101,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(101, 19), + // (200,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(200, 13), + // (201,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(201, 19), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(300, 13), + // (301,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 19), + // (400,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(400, 13), + // (401,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(401, 19) + ); + } + + [Fact] + public void NonBoxingUnionMatching_76_TryGetValue_NullableAnalysis_Generic() + { + var src = @" +#nullable enable + +[System.Runtime.CompilerServices.Union] +struct S1 +{ + public S1(string? x) => throw null!; + public S1(bool? x) => throw null!; + public object? Value => throw null!; + public T TryGetValue(out string? x) => throw null!; +} + +class Program +{ + static void Test2(S1 s) + { + if (s.TryGetValue(out var value)) + { +#line 100 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 200 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } + + static void Test4(S1 s) + { + if (!s.TryGetValue(out var value)) + { +#line 300 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + else + { +#line 400 + value.ToString(); + _ = s switch { string => 1, bool => 3 }; + } + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (100,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(100, 13), + // (101,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(101, 19), + // (200,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(200, 13), + // (201,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(201, 19), + // (300,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(300, 13), + // (301,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(301, 19), + // (400,13): warning CS8602: Dereference of a possibly null reference. + // value.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "value").WithLocation(400, 13), + // (401,19): warning CS8655: The switch expression does not handle some null inputs (it is not exhaustive). For example, the pattern 'null' is not covered. + // _ = s switch { string => 1, bool => 3 }; + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveForNull, "switch").WithArguments("null").WithLocation(401, 19) + ); + } + + [Fact] + public void NonBoxingUnionMatching_77_TryGetValue() + { + var src = @" +[System.Runtime.CompilerServices.Union] +struct S1 +{ + private readonly object _value; + public S1(int? x) { _value = x; } + public S1(string x) { _value = x; } + public object Value => _value; + public bool TryGetValue(out int x) { if (_value is int v) { x = v; return true; } x = 0; return false; } + + static void Main() + { + System.Console.Write(Test1(new S1(1))); + System.Console.Write(Test1(new S1())); + System.Console.Write(Test1(new S1(""a""))); + System.Console.Write(Test2(new S1(2))); + System.Console.Write(Test2(new S1())); + System.Console.Write(Test2(new S1(""b""))); + } + + static bool Test1(S1 u) + { + return u is int; + } + + static bool Test2(S1 u) + { + return u is not int; + } +} +"; + var comp = CreateCompilation([src, UnionAttributeSource], options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "TrueFalseFalseFalseTrueTrue").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 10 (0xa) + .maxstack 2 + .locals init (int V_0) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out int)"" + IL_0009: ret +} +"); + + verifier.VerifyIL("S1.Test2", @" +{ + // Code size 13 (0xd) + .maxstack 2 + .locals init (int V_0) + IL_0000: ldarga.s V_0 + IL_0002: ldloca.s V_0 + IL_0004: call ""bool S1.TryGetValue(out int)"" + IL_0009: ldc.i4.0 + IL_000a: ceq + IL_000c: ret +} +"); + } + + [Fact] + public void UnionDeclaration_01() + { + var unionSrc = @" +public +#line 100 +union S1(bool, int) +{ +} +"; + var consumer = @" +class Program +{ + static void Main() + { + System.Console.Write(Test(10)); + System.Console.Write(Test(11)); + System.Console.Write(Test(true)); + System.Console.Write(Test(false)); + System.Console.Write(Test(default)); + } + + static int Test(S1 u) + { + return u switch + { + 10 => 1, + true => 2, + int => 3, + bool => 4, + _ => 5 + }; + } +} +"; + + var comp1 = CreateCompilation([unionSrc, UnionAttributeSource, IUnionSource, consumer], options: TestOptions.DebugExe); + var s1 = comp1.GetTypeByMetadataName("S1"); + Assert.True(s1.IsUnionType); + Assert.False(s1.IsRecordStruct); + Assert.False(s1.IsRecord); + + VerifyCaseTypes(comp1, "S1", ["System.Boolean", "System.Int32"]); + + var members = s1.GetMembers(); + Assert.Equal(6, members.Length); + + AssertEx.SequenceEqual(["System.Object? S1.Value.field", "System.Object? S1.Value { get; }", "readonly System.Object? S1.Value.get", + "S1.S1(System.Boolean value)", "S1.S1(System.Int32 value)", "S1.S1()"], + members.Select(s => s.ToTestDisplayString(includeNonNullable: true))); + + Assert.False(members[0].IsStatic); + Assert.True(members[0].IsImplicitlyDeclared); + Assert.False(members[1].IsStatic); + Assert.True(members[1].IsImplicitlyDeclared); + Assert.False(members[2].IsStatic); + Assert.True(members[2].IsImplicitlyDeclared); + Assert.False(members[^3].IsStatic); + Assert.True(members[^3].IsImplicitlyDeclared); + Assert.False(members[^2].IsStatic); + Assert.True(members[^2].IsImplicitlyDeclared); + + var tree = comp1.SyntaxTrees.First(); + var model = comp1.GetSemanticModel(tree); + var s1Decl = tree.GetRoot().DescendantNodes().OfType().Single(); + + Assert.Equal("S1", s1Decl.Identifier.ToString()); + + Assert.Empty(members[0].DeclaringSyntaxReferences); + Assert.Equal(s1Decl, members[1].DeclaringSyntaxReferences.Single().GetSyntax()); + Assert.Equal(s1Decl, members[2].DeclaringSyntaxReferences.Single().GetSyntax()); + Assert.Empty(members[^3].DeclaringSyntaxReferences); + Assert.Empty(members[^2].DeclaringSyntaxReferences); + + var location = members[0].Locations.Single(); + Assert.Equal(members[1].Locations.Single(), location); + location = members[1].Locations.Single(); + Assert.Equal(s1Decl, location.SourceTree.GetRoot().FindNode(location.SourceSpan)); + location = members[2].Locations.Single(); + Assert.Equal(s1Decl, location.SourceTree.GetRoot().FindNode(location.SourceSpan)); + location = members[^3].Locations.Single(); + Assert.Equal("bool", location.SourceTree.GetRoot().FindNode(location.SourceSpan).ToString()); + location = members[^2].Locations.Single(); + Assert.Equal("int", location.SourceTree.GetRoot().FindNode(location.SourceSpan).ToString()); + + Assert.Same(s1, model.GetDeclaredSymbol(s1Decl).GetSymbol()); + Assert.Null(model.GetDeclaredSymbol(s1Decl.ParameterList)); + Assert.Null(model.GetDeclaredSymbol(s1Decl.ParameterList.Parameters[0])); + Assert.Null(model.GetDeclaredSymbol(s1Decl.ParameterList.Parameters[0].Type)); + Assert.Null(model.GetDeclaredSymbol(s1Decl.ParameterList.Parameters[1])); + Assert.Null(model.GetDeclaredSymbol(s1Decl.ParameterList.Parameters[1].Type)); + + var typeInfo = model.GetTypeInfo(s1Decl.ParameterList.Parameters[0].Type); + Assert.Equal("System.Boolean", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("System.Boolean", typeInfo.ConvertedType.ToTestDisplayString()); + + typeInfo = model.GetTypeInfo(s1Decl.ParameterList.Parameters[1].Type); + Assert.Equal("System.Int32", typeInfo.Type.ToTestDisplayString()); + Assert.Equal("System.Int32", typeInfo.ConvertedType.ToTestDisplayString()); + + var verifier = CompileAndVerify(comp1, expectedOutput: "13245").VerifyDiagnostics(); + + verifier.VerifyTypeIL("S1", @" +.class public sequential ansi sealed beforefieldinit S1 + extends [netstandard]System.ValueType + implements System.Runtime.CompilerServices.IUnion +{ + .custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( + 01 00 02 00 00 + ) + .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( + 01 00 00 00 00 + ) + .custom instance void System.Runtime.CompilerServices.UnionAttribute::.ctor() = ( + 01 00 00 00 + ) + .interfaceimpl type System.Runtime.CompilerServices.IUnion + .custom instance void System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( + 01 00 00 00 00 + ) + // Fields + .field private initonly object 'k__BackingField' + .custom instance void [netstandard]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + .custom instance void [netstandard]System.Diagnostics.DebuggerBrowsableAttribute::.ctor(valuetype [netstandard]System.Diagnostics.DebuggerBrowsableState) = ( + 01 00 00 00 00 00 00 00 + ) + // Methods + .method public final hidebysig specialname newslot virtual + instance object get_Value () cil managed + { + .custom instance void System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( + 01 00 00 00 + ) + .custom instance void [netstandard]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x20a2 + // Code size 7 (0x7) + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldfld object S1::'k__BackingField' + IL_0006: ret + } // end of method S1::get_Value + .method public hidebysig specialname rtspecialname + instance void .ctor ( + bool 'value' + ) cil managed + { + .custom instance void [netstandard]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x20aa + // Code size 14 (0xe) + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: box [netstandard]System.Boolean + IL_0007: stfld object S1::'k__BackingField' + IL_000c: nop + IL_000d: ret + } // end of method S1::.ctor + .method public hidebysig specialname rtspecialname + instance void .ctor ( + int32 'value' + ) cil managed + { + .custom instance void [netstandard]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x20b9 + // Code size 14 (0xe) + .maxstack 8 + IL_0000: ldarg.0 + IL_0001: ldarg.1 + IL_0002: box [netstandard]System.Int32 + IL_0007: stfld object S1::'k__BackingField' + IL_000c: nop + IL_000d: ret + } // end of method S1::.ctor + // Properties + .property instance object Value() + { + .get instance object S1::get_Value() + } +} // end of class S1 +".Replace("[netstandard]", ExecutionConditionUtil.IsMonoOrCoreClr ? "[netstandard]" : "[mscorlib]")); + + var comp2 = CreateCompilation(consumer, references: [verifier.GetImageReference()], options: TestOptions.DebugExe); + var s12 = comp2.GetTypeByMetadataName("S1"); + Assert.True(s12.IsUnionType); + VerifyCaseTypes(comp2, "S1", ["System.Boolean", "System.Int32"]); + + members = s12.GetMembers(); + + AssertEx.SequenceEqual(["System.Object? S1.k__BackingField", "S1.S1()", "readonly System.Object? S1.Value.get", + "S1.S1(System.Boolean value)", "S1.S1(System.Int32 value)", "readonly System.Object? S1.Value { get; }"], + members.Select(s => s.ToTestDisplayString(includeNonNullable: true))); + + CompileAndVerify(comp2, expectedOutput: "13245").VerifyDiagnostics(); + + var unionAttributeSource = @" +namespace System.Runtime.CompilerServices +{ + public class UnionAttribute : System.Attribute + { + } +} +"; + var ref1 = CreateCompilation(unionAttributeSource).EmitToImageReference(); + var ref2 = CreateCompilation(unionAttributeSource).EmitToImageReference(); + + var comp3 = CreateCompilation([unionSrc, IUnionSource], references: [ref1, ref2]); + comp3.VerifyEmitDiagnostics( + // (100,7): error CS0656: Missing compiler required member 'System.Runtime.CompilerServices.UnionAttribute..ctor' + // union S1(bool, int) + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "S1").WithArguments("System.Runtime.CompilerServices.UnionAttribute", ".ctor").WithLocation(100, 7) + ); + + var comp4 = CreateCompilation(["extern alias ref1; [ref1::System.Runtime.CompilerServices.Union]" + unionSrc, IUnionSource], references: [ref1.WithAliases(["ref1"]), ref2.WithAliases(["ref2"])]); + verifier = CompileAndVerify( + comp4, + symbolValidator: (m) => + { + var s1 = m.GlobalNamespace.GetTypeMember("S1"); + Assert.Equal("S1", s1.Name); + CSharpAttributeData attr = s1.GetAttributes().Where(a => a.AttributeClass.Name.StartsWith("Union")).Single(); + AssertEx.Equal("System.Runtime.CompilerServices.UnionAttribute", attr.ToString()); + Assert.NotEqual(s1.ContainingModule, attr.AttributeClass.ContainingModule); + }).VerifyDiagnostics(); + + var comp5 = CreateCompilation(consumer, references: [verifier.GetImageReference()], options: TestOptions.DebugExe); + CompileAndVerify(comp5, expectedOutput: "13245").VerifyDiagnostics(); + + var comp6 = CreateCompilation(["[System.Runtime.CompilerServices.Union]" + unionSrc, UnionAttributeSource, IUnionSource], references: [ref1.WithAliases(["ref1"]), ref2.WithAliases(["ref2"])]); + verifier = CompileAndVerify( + comp6, + symbolValidator: (m) => + { + var s1 = m.GlobalNamespace.GetTypeMember("S1"); + Assert.Equal("S1", s1.Name); + CSharpAttributeData attr = s1.GetAttributes().Where(a => a.AttributeClass.Name.StartsWith("Union")).Single(); + AssertEx.Equal("System.Runtime.CompilerServices.UnionAttribute", attr.ToString()); + Assert.Same(s1.ContainingModule, attr.AttributeClass.ContainingModule); + }).VerifyDiagnostics(); + + var comp7 = CreateCompilation(consumer, references: [verifier.GetImageReference()], options: TestOptions.DebugExe); + CompileAndVerify(comp7, expectedOutput: "13245").VerifyDiagnostics(); + } + + [Fact] + public void UnionDeclaration_02() + { + var src = @" +partial +#line 100 +union S1(int, bool) +{ +} + +partial +#line 200 +union S1(int, long) +{ +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + comp.VerifyEmitDiagnostics( + // (200,9): error CS8863: Only a single partial type declaration may have a parameter list + // union S1(int, long) + Diagnostic(ErrorCode.ERR_MultipleRecordParameterLists, "(int, long)").WithLocation(200, 9) + ); + + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", ["System.Int32", "System.Boolean"]); + } + + [Fact] + public void UnionDeclaration_03() + { + var src = @" +partial union S1(int, bool) +{ +} + +partial union S1 +{ +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + comp.VerifyEmitDiagnostics(); + + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", ["System.Int32", "System.Boolean"]); + } + + [Fact] + public void UnionDeclaration_04() + { + var src = @" +partial union S1 +{ +} + +partial union S1(int, bool) +{ +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + comp.VerifyEmitDiagnostics(); + + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", ["System.Int32", "System.Boolean"]); + } + + [Fact] + public void UnionDeclaration_05() + { + var src = @" +partial struct S1 +{ +} + +partial union S1(int, bool) +{ +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + comp.VerifyEmitDiagnostics( + // (6,15): error CS0261: Partial declarations of 'S1' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + // partial union S1(int, bool) + Diagnostic(ErrorCode.ERR_PartialTypeKindConflict, "S1").WithArguments("S1").WithLocation(6, 15) + ); + } + + [Fact] + public void UnionDeclaration_06() + { + var src = @" +partial union S1(int, bool) +{ +} + +partial record S1 +{ +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + comp.VerifyEmitDiagnostics( + // (6,16): error CS0261: Partial declarations of 'S1' must be all classes, all record classes, all structs, all unions, all record structs, or all interfaces + // partial record S1 + Diagnostic(ErrorCode.ERR_PartialTypeKindConflict, "S1").WithArguments("S1").WithLocation(6, 16) + ); + } + + [Fact] + public void UnionDeclaration_07() + { + var src = @" +static union S1(int, bool) +{ +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + comp.VerifyEmitDiagnostics( + // (2,14): error CS0106: The modifier 'static' is not valid for this item + // static union S1(int, bool) + Diagnostic(ErrorCode.ERR_BadMemberFlag, "S1").WithArguments("static").WithLocation(2, 14) + ); + } + + [Fact] + public void UnionDeclaration_08() + { + var src = @" +#line 100 +union S1(int, int) +{ +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + + // https://github.com/dotnet/roslyn/issues/82636: Consider reporting a more informative error. + comp.VerifyEmitDiagnostics( + // (100,15): error CS0111: Type 'S1' already defines a member called 'S1' with the same parameter types + // union S1(int, int) + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "int").WithArguments("S1", "S1").WithLocation(100, 15) + ); + } + + [Fact] + public void UnionDeclaration_09() + { + var src = @" +#line 100 +union S1(int, __arglist) +{ +} +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + comp.VerifyEmitDiagnostics( + // (100,15): error CS1669: __arglist is not valid in this context + // union S1(int, __arglist) + Diagnostic(ErrorCode.ERR_IllegalVarArgs, "__arglist").WithLocation(100, 15) + ); + } + + [Fact] + public void UnionDeclaration_10() + { + var src = @" +#line 100 +union S1; +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + + comp.VerifyEmitDiagnostics( + // (100,7): error CS9370: A union declaration must specify at least one case type. + // union S1; + Diagnostic(ErrorCode.ERR_UnionDeclarationNeedsCaseTypes, "S1").WithLocation(100, 7) + ); + + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", []); + } + + [Fact] + public void UnionDeclaration_11() + { + var src = @" +#line 100 +union S1(); +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + + // https://github.com/dotnet/roslyn/issues/82636: Consider repoting a more informative error. Perhaps something like: "A union declaration must specify at least one case type." + comp.VerifyEmitDiagnostics( + // (100,10): error CS1031: Type expected + // union S1(); + Diagnostic(ErrorCode.ERR_TypeExpected, ")").WithLocation(100, 10) + ); + + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", ["?"]); + } + + [Fact] + public void UnionDeclaration_12_MissingUnionAttribute() + { + var unionSrc = @" +#line 2 +union S1(int, bool) +{ +} +"; + + var comp = CreateCompilation([unionSrc, IUnionSource]); + comp.VerifyEmitDiagnostics( + // (2,7): error CS0656: Missing compiler required member 'System.Runtime.CompilerServices.UnionAttribute..ctor' + // union S1(int, bool) + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "S1").WithArguments("System.Runtime.CompilerServices.UnionAttribute", ".ctor").WithLocation(2, 7) + ); + + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", ["System.Int32", "System.Boolean"]); + } + + [Fact] + public void UnionDeclaration_13() + { + var src = @" +union S1( +#nullable enable + string? +#nullable restore +); +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", ["System.String"]); + + CompileAndVerify(comp, symbolValidator: verify, sourceSymbolValidator: verify).VerifyDiagnostics(); + + void verify(ModuleSymbol m) + { + var s1 = m.GlobalNamespace.GetTypeMember("S1"); + AssertEx.Equal("S1..ctor(System.String? value)", s1.InstanceConstructors.Where(c => c.ParameterCount == 1).Single().ToTestDisplayString()); + } + } + + [Fact] + public void UnionDeclaration_14() + { + var src = @" +union S1(T); +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + Assert.True(comp.GetTypeByMetadataName("S1`1").IsUnionType); + VerifyCaseTypes(comp, "S1`1", ["T"]); + + comp.VerifyEmitDiagnostics(); + } + + [Fact] + public void UnionDeclaration_15() + { + var src = @" +#line 100 +union S1(System.ArgIterator, int); +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource], targetFramework: TargetFramework.NetCoreApp); + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", ["System.ArgIterator", "System.Int32"]); + + comp.VerifyEmitDiagnostics( + // (100,10): error CS9371: Cannot convert type 'ArgIterator' to 'object' via an implicit reference or boxing conversion + // union S1(System.ArgIterator, int); + Diagnostic(ErrorCode.ERR_NoImplicitConversionToObject, "System.ArgIterator").WithArguments("System.ArgIterator").WithLocation(100, 10) + ); + } + + [Fact] + public void UnionDeclaration_16() + { + var src = @" +#pragma warning disable CS1718 // Comparison made to same variable; did you mean to compare something else? + +union S1(C1); + +class C1 +{ + public override int GetHashCode() => 1; + public override bool Equals(object obj) => obj is C1; +} + +class Program +{ + static void Main() + { + var s11 = new S1(new C1()); + var s12 = new S1(new C1()); + var s13 = new S1(); + System.Console.WriteLine(s11.ToString()); + System.Console.WriteLine(s13.ToString()); + System.Console.WriteLine(s11.Equals(s11)); + System.Console.WriteLine(s11.Equals(s12)); + System.Console.WriteLine(s11.Equals(s13)); + System.Console.WriteLine(s13.Equals(s13)); + } +} +"; + + var comp1 = CreateCompilation([src, UnionAttributeSource, IUnionSource], options: TestOptions.DebugExe); + CompileAndVerify(comp1, expectedOutput: @" +S1 +S1 +True +True +False +True +").VerifyDiagnostics(); + } + + [Fact] + public void UnionDeclaration_17() + { + var src = @" +union S1(C1) +{ + public void OtherMember() + { + System.Console.Write(1); + } +} + +class C1 +{ +} + +class Program +{ + static void Main() + { + default(S1).OtherMember(); + } +} +"; + + var comp1 = CreateCompilation([src, UnionAttributeSource, IUnionSource], options: TestOptions.DebugExe); + CompileAndVerify(comp1, expectedOutput: "1").VerifyDiagnostics(); + } + + [Fact] + public void UnionDeclaration_18() + { + var src = @" +#line 100 +union S1(System.Nullable); +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource], targetFramework: TargetFramework.NetCoreApp); + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", ["System.String"]); + + comp.VerifyEmitDiagnostics( + // (100,10): error CS9371: Cannot convert type 'string?' to 'object' via an implicit reference or boxing conversion + // union S1(System.Nullable); + Diagnostic(ErrorCode.ERR_NoImplicitConversionToObject, "System.Nullable").WithArguments("string?").WithLocation(100, 10), + // (100,10): error CS0453: The type 'string' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'Nullable' + // union S1(System.Nullable); + Diagnostic(ErrorCode.ERR_ValConstraintNotSatisfied, "System.Nullable").WithArguments("System.Nullable", "T", "string").WithLocation(100, 10) + ); + } + + [Fact] + public void UnionDeclaration_19_MissingObject() + { + var src = @" +#line 100 +union S1(int); +"; + var comp = CreateCompilation([src, UnionAttributeSource, IUnionSource]); + comp.MakeTypeMissing(SpecialType.System_Object); + + comp.VerifyEmitDiagnostics( + // (100,7): error CS0518: Predefined type 'System.Object' is not defined or imported + // union S1(int); + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "S1").WithArguments("System.Object").WithLocation(100, 7), + // (100,7): error CS0518: Predefined type 'System.Object' is not defined or imported + // union S1(int); + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "S1").WithArguments("System.Object").WithLocation(100, 7), + // (100000,9): error CS0518: Predefined type 'System.Object' is not defined or imported + // object? Value { get; } + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "object").WithArguments("System.Object").WithLocation(100000, 9) + ); + } + + [Fact] + public void UnionDeclaration_20() + { + var src = @" +#pragma warning disable CS1718 // Comparison made to same variable; did you mean to compare something else? + +union S1(object); + + +class Program +{ + static void Main() + { + var s11 = new S1(123); + System.Console.WriteLine(s11.Value); + } +} +"; + + var comp1 = CreateCompilation([src, UnionAttributeSource, IUnionSource], options: TestOptions.DebugExe); + CompileAndVerify(comp1, expectedOutput: @"123").VerifyDiagnostics(); + } + + [Fact] + public void UnionDeclaration_21() + { + var src = @" +#pragma warning disable CS1718 // Comparison made to same variable; did you mean to compare something else? + +union S1(int?); + + +class Program +{ + static void Main() + { + var s11 = new S1(123); + System.Console.WriteLine(s11.Value); + } +} +"; + + var comp1 = CreateCompilation([src, UnionAttributeSource, IUnionSource], options: TestOptions.DebugExe); + CompileAndVerify(comp1, expectedOutput: @"123").VerifyDiagnostics(); + } + + [Fact] + public void UnionDeclaration_22_IUnion_Missing() + { + var unionSrc = @" +#line 2 +union S1(int, bool) +{ +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource]); + comp.VerifyEmitDiagnostics( + // (2,7): error CS0518: Predefined type 'System.Runtime.CompilerServices.IUnion' is not defined or imported + // union S1(int, bool) + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "S1").WithArguments("System.Runtime.CompilerServices.IUnion").WithLocation(2, 7) + ); + + Assert.True(comp.GetTypeByMetadataName("S1").IsUnionType); + VerifyCaseTypes(comp, "S1", ["System.Int32", "System.Boolean"]); + } + + [Fact] + public void UnionDeclaration_22_IUnion_InBaseInterfaces() + { + var unionSrc = @" +union S1(int, bool) : System.Runtime.CompilerServices.IUnion +{ +} + +class Program +{ + static void Main() + { + System.Console.Write(Test(10)); + System.Console.Write(Test(11)); + System.Console.Write(Test(true)); + System.Console.Write(Test(false)); + System.Console.Write(Test(default)); + } + + static int Test(S1 u) + { + return ((System.Runtime.CompilerServices.IUnion)u).Value switch + { + 10 => 1, + true => 2, + int => 3, + bool => 4, + _ => 5 + }; + } +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource, IUnionSource], options: TestOptions.DebugExe); + CompileAndVerify(comp, symbolValidator: checkInterfaces, sourceSymbolValidator: checkInterfaces, expectedOutput: "13245").VerifyDiagnostics(); + + void checkInterfaces(ModuleSymbol m) + { + var s1 = m.GlobalNamespace.GetTypeMember("S1"); + Assert.Equal("System.Runtime.CompilerServices.IUnion", s1.InterfacesNoUseSiteDiagnostics().Single().ToTestDisplayString()); + } + } + + [Fact] + public void UnionDeclaration_23_IUnion() + { + var unionSrc = @" +union S1(int, bool) +{ +} + +class Program +{ + static void Main() + { + System.Console.Write(Test(10)); + System.Console.Write(Test(11)); + System.Console.Write(Test(true)); + System.Console.Write(Test(false)); + System.Console.Write(Test(default)); + } + + static int Test(S1 u) + { + return ((System.Runtime.CompilerServices.IUnion)u).Value switch + { + 10 => 1, + true => 2, + int => 3, + bool => 4, + _ => 5 + }; + } +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource, IUnionSource], options: TestOptions.DebugExe); + CompileAndVerify(comp, symbolValidator: checkInterfaces, sourceSymbolValidator: checkInterfaces, expectedOutput: "13245").VerifyDiagnostics(); + + void checkInterfaces(ModuleSymbol m) + { + var s1 = m.GlobalNamespace.GetTypeMember("S1"); + Assert.Equal("System.Runtime.CompilerServices.IUnion", s1.InterfacesNoUseSiteDiagnostics().Single().ToTestDisplayString()); + } + } + + [Fact] + public void UnionDeclaration_24_IUnion_ValueNullabilityMismatch() + { + var unionSrc = @" +union S1(int, bool) +{ +} + +namespace System.Runtime.CompilerServices +{ + public interface IUnion + { +#nullable enable + object Value { get; } +#nullable disable + } +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource]); + CompileAndVerify(comp).VerifyDiagnostics(); + } + + [Fact] + public void UnionDeclaration_25_IUnion_ValueMissing() + { + var unionSrc = @" +union S1(int, bool) +{ +} + +namespace System.Runtime.CompilerServices +{ + public interface IUnion + { + } +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource]); + CompileAndVerify(comp).VerifyDiagnostics(); + } + + [Fact] + public void UnionDeclaration_26_IUnion_UnexpectedMember() + { + var unionSrc = @" +union S1(int, bool) +{ +} + +union S2(int, bool) +{ + public void M() { } +} + +union S3(int, bool) +{ + void System.Runtime.CompilerServices.IUnion.M() { } +} + +namespace System.Runtime.CompilerServices +{ + public interface IUnion + { + void M(); + } +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource]); + comp.VerifyDiagnostics( + // (2,7): error CS0535: 'S1' does not implement interface member 'IUnion.M()' + // union S1(int, bool) + Diagnostic(ErrorCode.ERR_UnimplementedInterfaceMember, "S1").WithArguments("S1", "System.Runtime.CompilerServices.IUnion.M()").WithLocation(2, 7) + ); + } + + [Fact] + public void UnionDeclaration_27_IUnion() + { + var unionSrc = @" +union S1(int, bool) : I1 +{ +} + +interface I1 : System.Runtime.CompilerServices.IUnion; + +class Program +{ + static void Main() + { + System.Console.Write(Test(10)); + System.Console.Write(Test(11)); + System.Console.Write(Test(true)); + System.Console.Write(Test(false)); + System.Console.Write(Test(default)); + } + + static int Test(S1 u) + { + return ((System.Runtime.CompilerServices.IUnion)u).Value switch + { + 10 => 1, + true => 2, + int => 3, + bool => 4, + _ => 5 + }; + } +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource, IUnionSource], options: TestOptions.DebugExe); + CompileAndVerify(comp, symbolValidator: checkInterfaces, sourceSymbolValidator: checkInterfaces, expectedOutput: "13245").VerifyDiagnostics(); + + void checkInterfaces(ModuleSymbol m) + { + var s1 = m.GlobalNamespace.GetTypeMember("S1"); + AssertEx.SequenceEqual(["I1", "System.Runtime.CompilerServices.IUnion"], s1.InterfacesNoUseSiteDiagnostics().ToTestDisplayStrings()); + } + } + + [Fact] + public void UnionDeclaration_28() + { + var unionSrc = @" +#pragma warning disable CS0169 // The field 'S1.F' is never used +union S1(int, bool) +{ + int F1; +} + +union S2(int, bool) +{ + static int F2; +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource, IUnionSource]); + comp.VerifyDiagnostics( + // (5,9): error CS9373: Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + // int F1; + Diagnostic(ErrorCode.ERR_InstanceFieldInUnion, "F1").WithLocation(5, 9) + ); + } + + [Fact] + public void UnionDeclaration_29() + { + var unionSrc = @" +union S1(int, bool) +{ + int P1 { get => 1; set {}} +} + +union S2(int, bool) +{ + int P2 { get; } +} + +union S3(int, bool) +{ + int P3 { set {field = value;} } +} + +union S4(int, bool) +{ + int P4 { get; set;} +} + +union S5(int, bool) +{ + int P5 { get => field; set {field = value;}} +} + +interface I1 +{ + int P0 { get; set; } +} + +union S6(int, bool) : I1 +{ + int I1.P0 { get; set; } +} + +union S7(int, bool) +{ + static int P7 { get; set;} +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource, IUnionSource]); + comp.VerifyDiagnostics( + // (9,9): error CS9373: Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + // int P2 { get; } + Diagnostic(ErrorCode.ERR_InstanceFieldInUnion, "P2").WithLocation(9, 9), + // (14,9): error CS9373: Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + // int P3 { set {field = value;} } + Diagnostic(ErrorCode.ERR_InstanceFieldInUnion, "P3").WithLocation(14, 9), + // (19,9): error CS9373: Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + // int P4 { get; set;} + Diagnostic(ErrorCode.ERR_InstanceFieldInUnion, "P4").WithLocation(19, 9), + // (24,9): error CS9373: Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + // int P5 { get => field; set {field = value;}} + Diagnostic(ErrorCode.ERR_InstanceFieldInUnion, "P5").WithLocation(24, 9), + // (34,12): error CS9373: Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + // int I1.P0 { get; set; } + Diagnostic(ErrorCode.ERR_InstanceFieldInUnion, "P0").WithLocation(34, 12) + ); + } + + [Fact] + public void UnionDeclaration_30() + { + var unionSrc = @" +#pragma warning disable CS0067 // The event 'S2.E2' is never used + +union S1(int, bool) +{ + event System.Action E1 { add{} remove{}} +} + +union S2(int, bool) +{ + event System.Action E2; +} + +interface I1 +{ + event System.Action E0; +} + +union S3(int, bool) : I1 +{ + event System.Action I1.E0; +} + +union S4(int, bool) +{ + static event System.Action E4; +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource, IUnionSource]); + comp.VerifyDiagnostics( + // (11,25): error CS9373: Instance fields, auto-properties or field-like events are not permitted in a 'union' declaration. + // event System.Action E2; + Diagnostic(ErrorCode.ERR_InstanceFieldInUnion, "E2").WithLocation(11, 25), + // (19,23): error CS0535: 'S3' does not implement interface member 'I1.E0.add' + // union S3(int, bool) : I1 + Diagnostic(ErrorCode.ERR_UnimplementedInterfaceMember, "I1").WithArguments("S3", "I1.E0.add").WithLocation(19, 23), + // (19,23): error CS0535: 'S3' does not implement interface member 'I1.E0.remove' + // union S3(int, bool) : I1 + Diagnostic(ErrorCode.ERR_UnimplementedInterfaceMember, "I1").WithArguments("S3", "I1.E0.remove").WithLocation(19, 23), + // (21,28): error CS0071: An explicit interface implementation of an event must use event accessor syntax + // event System.Action I1.E0; + Diagnostic(ErrorCode.ERR_ExplicitEventFieldImpl, "E0").WithLocation(21, 28) + ); + } + + [Fact] + public void UnionDeclaration_31() + { + var unionSrc = @" +union S1(int, bool) +{ + S1(string x) + : this(1) {} +} + +union S2(int, bool) +{ + S2(ref string x) + : this(1) {} +} + +union S3(int, bool) +{ + S3(in string x) + : this(1) {} +} + +union S4(int, bool) +{ + S4(ref readonly string x) + : this(1) {} +} + +union S5(int, bool) +{ + S5(out string x) + : this(1) { x = """"; } +} + +union S6(int, bool) +{ + S6(int x, bool y) + : this(x) {} +} + +union S7(int, bool) +{ + public S7() + : this(1) {} +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource, IUnionSource]); + comp.VerifyDiagnostics( + // (4,5): error CS9374: Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + // S1(string x) + Diagnostic(ErrorCode.ERR_InstanceCtorWithOneParameterInUnion, "S1").WithLocation(4, 5), + // (10,5): error CS9374: Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + // S2(ref string x) + Diagnostic(ErrorCode.ERR_InstanceCtorWithOneParameterInUnion, "S2").WithLocation(10, 5), + // (16,5): error CS9374: Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + // S3(in string x) + Diagnostic(ErrorCode.ERR_InstanceCtorWithOneParameterInUnion, "S3").WithLocation(16, 5), + // (22,5): error CS9374: Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + // S4(ref readonly string x) + Diagnostic(ErrorCode.ERR_InstanceCtorWithOneParameterInUnion, "S4").WithLocation(22, 5), + // (28,5): error CS9374: Explicitly declared public constructors with a single parameter are not permitted in a 'union' declaration. + // S5(out string x) + Diagnostic(ErrorCode.ERR_InstanceCtorWithOneParameterInUnion, "S5").WithLocation(28, 5) + ); + } + + [Fact] + public void UnionDeclaration_32() + { + var unionSrc = @" +union S6(int, bool) +{ +#line 4 + S6(int x, bool y) + {} +} + +union S7(int, bool) +{ +#line 10 + public S7() + {} +} + +union S8(int, bool) +{ +#line 16 + S8(int x, bool y) + {} + + S8(string x, bool y) + : this(1) + {} +} + +union S9(int, bool) +{ + S9(int x, bool y) + : this() + {} + +#line 30 + public S9() + {} +} + +union S10(int, bool) +{ +#line 36 + S10(int x, bool y) + {} + + public S10() + : this(1) + {} +} + +union S11(int, bool) +{ + static S11() + {} +} + +union S12(int, bool) +{ + S12(int x, bool y) +#line 53 + : this() + {} +} + +union S13(int, bool) +{ + S13(int x, bool y) + : this("""", y) + {} + + S13(string x, bool y) + : this(y) + {} +} +"; + + var comp = CreateCompilation([unionSrc, UnionAttributeSource, IUnionSource]); + comp.VerifyDiagnostics( + // (4,5): error CS9375: A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + // S6(int x, bool y) + Diagnostic(ErrorCode.ERR_UnionConstructorCallsDefaultConstructor, "S6").WithLocation(4, 5), + // (10,12): error CS9375: A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + // public S7() + Diagnostic(ErrorCode.ERR_UnionConstructorCallsDefaultConstructor, "S7").WithLocation(10, 12), + // (16,5): error CS9375: A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + // S8(int x, bool y) + Diagnostic(ErrorCode.ERR_UnionConstructorCallsDefaultConstructor, "S8").WithLocation(16, 5), + // (30,12): error CS9375: A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + // public S9() + Diagnostic(ErrorCode.ERR_UnionConstructorCallsDefaultConstructor, "S9").WithLocation(30, 12), + // (36,5): error CS9375: A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + // S10(int x, bool y) + Diagnostic(ErrorCode.ERR_UnionConstructorCallsDefaultConstructor, "S10").WithLocation(36, 5), + // (53,7): error CS9375: A constructor declared in a 'union' declaration must have a 'this' initializer that calls a synthesized constructor or an explicitly declared constructor. + // : this() + Diagnostic(ErrorCode.ERR_UnionConstructorCallsDefaultConstructor, "this").WithLocation(53, 7) + ); + } + } +} + +// https://github.com/dotnet/roslyn/issues/82636: Test conversions from 'new()' and other target-typed constructs +// https://github.com/dotnet/roslyn/issues/82636: Ensure we test nullable state resulting from a struct union type nullary (no-argument) constructor invocation. diff --git a/src/Compilers/CSharp/Test/CSharp15/UnsafeEvolutionTests.cs b/src/Compilers/CSharp/Test/CSharp15/UnsafeEvolutionTests.cs new file mode 100644 index 000000000000..92dfc5ca8f7e --- /dev/null +++ b/src/Compilers/CSharp/Test/CSharp15/UnsafeEvolutionTests.cs @@ -0,0 +1,10282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Symbols; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Roslyn.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; + +[CompilerTrait(CompilerFeature.Unsafe)] +public sealed class UnsafeEvolutionTests : CompilingTestBase +{ + /// See . + /// See . + private void CompileAndVerifyUnsafe( + string lib, + string caller, + object[] expectedUnsafeSymbols, + object[] expectedSafeSymbols, + DiagnosticDescription[] expectedDiagnostics, + ReadOnlySpan additionalSources = default, + Verification verify = default, + CallerUnsafeMode expectedUnsafeMode = CallerUnsafeMode.Explicit, + object[]? expectedNoAttributeInSource = null, + object[]? expectedNoAttributeUnderLegacyRules = null, + object[]? skipSymbolsInSource = null, + CSharpParseOptions? parseOptions = null, + CSharpCompilationOptions? optionsDll = null, + TargetFramework targetFramework = TargetFramework.Standard, + DiagnosticDescription[]? expectedDiagnosticsWhenReferencingLegacyLib = null, + DiagnosticDescription[]? expectedDiagnosticsForLegacyCaller = null) + { + optionsDll ??= TestOptions.UnsafeReleaseDll; + var optionsExe = optionsDll.WithOutputKind(OutputKind.ConsoleApplication); + + Assert.False(optionsDll.UseUpdatedMemorySafetyRules); + + CreateCompilation([lib, caller, .. additionalSources], + targetFramework: targetFramework, + parseOptions: parseOptions, + options: optionsExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + var libUpdated = CompileAndVerify([lib, .. additionalSources], + targetFramework: targetFramework, + parseOptions: parseOptions, + options: optionsDll.WithUpdatedMemorySafetyRules(), + verify: verify, + symbolValidator: symbolValidator) + .VerifyDiagnostics(); + + var libUpdatedRefs = new MetadataReference[] { libUpdated.GetImageReference(), libUpdated.Compilation.ToMetadataReference() }; + + foreach (var libUpdatedRef in libUpdatedRefs) + { + var libAssemblySymbol = CreateCompilation(caller, [libUpdatedRef], + targetFramework: targetFramework, + parseOptions: parseOptions, + options: optionsExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics) + .GetReferencedAssemblySymbol(libUpdatedRef); + + symbolValidator(libAssemblySymbol.Modules.Single()); + + // Updated-rules library referenced by legacy-rules caller. + CreateCompilation(caller, [libUpdatedRef], + targetFramework: targetFramework, + parseOptions: parseOptions, + options: optionsExe.AddSpecificDiagnosticOptions(GetIdForErrorCode(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules), ReportDiagnostic.Suppress)) + .VerifyDiagnostics(expectedDiagnosticsForLegacyCaller ?? []); + } + + var libLegacy = CompileAndVerify([lib, .. additionalSources], + targetFramework: targetFramework, + parseOptions: parseOptions, + options: optionsDll.AddSpecificDiagnosticOptions(GetIdForErrorCode(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules), ReportDiagnostic.Suppress), + verify: verify, + symbolValidator: module => + { + VerifyMemorySafetyRulesAttribute(module, includesAttributeDefinition: false, includesAttributeUse: false); + VerifyRequiresUnsafeAttribute( + module, + expectedUnsafeSymbols: expectedUnsafeSymbols, + expectedSafeSymbols: expectedSafeSymbols, + expectedNoAttributeInSource: expectedNoAttributeInSource, + expectedNoAttribute: expectedNoAttributeUnderLegacyRules, + expectedUnsafeMode: CallerUnsafeMode.None); + }) + .VerifyDiagnostics() + .GetImageReference(); + + CreateCompilation(caller, [libLegacy], + targetFramework: targetFramework, + parseOptions: parseOptions, + options: optionsExe.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(expectedDiagnosticsWhenReferencingLegacyLib ?? []); + + // Legacy-rules library referenced by legacy-rules caller. + CreateCompilation(caller, [libLegacy], + targetFramework: targetFramework, + parseOptions: parseOptions, + options: optionsExe.AddSpecificDiagnosticOptions(GetIdForErrorCode(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules), ReportDiagnostic.Suppress)) + .VerifyEmitDiagnostics(expectedDiagnosticsForLegacyCaller ?? []); + + void symbolValidator(ModuleSymbol module) + { + var isSource = module is SourceModuleSymbol; + VerifyMemorySafetyRulesAttribute( + module, + includesAttributeDefinition: !isSource, + includesAttributeUse: !isSource, + isSynthesized: isSource ? null : true); + VerifyRequiresUnsafeAttribute( + module, + expectedUnsafeSymbols: exceptSymbolsSkippedInSource(expectedUnsafeSymbols), + expectedSafeSymbols: exceptSymbolsSkippedInSource(expectedSafeSymbols), + expectedNoAttributeInSource: exceptSymbolsSkippedInSource(expectedNoAttributeInSource), + expectedUnsafeMode: expectedUnsafeMode); + + [return: NotNullIfNotNull(nameof(original))] + object[]? exceptSymbolsSkippedInSource(object[]? original) + { + return isSource && skipSymbolsInSource is [..] && original is [..] + ? original.Except(skipSymbolsInSource).ToArray() + : original; + } + } + } + + private static Func ExtensionMember(string containerName, string memberName) + { + return module => module.GlobalNamespace + .GetMember(containerName) + .GetMembers("") + .Cast() + .SelectMany(block => block.GetMembers(memberName)) + .SingleOrDefault() + ?? throw new InvalidOperationException($"Cannot find '{containerName}.{memberName}'."); + } + + private static Func Overload(string qualifiedName, int parameterCount) + { + return module => module.GlobalNamespace + .GetMembersByQualifiedName(qualifiedName) + .SingleOrDefault(m => m.Parameters.Length == parameterCount) + ?? throw new InvalidOperationException($"Cannot find '{qualifiedName}' with {parameterCount} parameters."); + } + + private static Func OverloadByReturnType(string qualifiedName, string returnType) + { + return module => + { + var candidates = module.GlobalNamespace + .GetMembersByQualifiedName(qualifiedName); + + return candidates.SingleOrDefault(m => m.ReturnType.ToTestDisplayString() == returnType) + ?? throw new InvalidOperationException($"Cannot find '{qualifiedName}' with return type '{returnType}'. " + + $"Found {string.Join(", ", candidates.Select(static m => m.ReturnType.ToTestDisplayString()))}."); + }; + } + + private static void VerifyMemorySafetyRulesAttribute( + ModuleSymbol module, + bool includesAttributeDefinition, + bool includesAttributeUse, + bool? isSynthesized = null) + { + const string Name = "MemorySafetyRulesAttribute"; + const string FullName = $"System.Runtime.CompilerServices.{Name}"; + var type = (NamedTypeSymbol)module.GlobalNamespace.GetMember(FullName); + var attribute = module.GetAttributes().SingleOrDefault(a => a.AttributeClass?.Name == Name); + + if (includesAttributeDefinition) + { + Assert.NotNull(type); + + Assert.NotNull(isSynthesized); + if (isSynthesized.Value) + { + var attributeAttributes = type.GetAttributes() + .Select(a => a.AttributeClass.ToTestDisplayString()) + .OrderBy(StringComparer.Ordinal); + Assert.Equal( + [ + "Microsoft.CodeAnalysis.EmbeddedAttribute", + "System.AttributeUsageAttribute", + "System.Runtime.CompilerServices.CompilerGeneratedAttribute", + ], + attributeAttributes); + } + } + else + { + Assert.Null(type); + if (includesAttributeUse) + { + Assert.NotNull(attribute); + type = attribute.AttributeClass; + } + } + + if (type is { }) + { + Assert.NotNull(isSynthesized); + Assert.Equal(isSynthesized.Value ? Accessibility.Internal : Accessibility.Public, type.DeclaredAccessibility); + } + else + { + Assert.Null(isSynthesized); + } + + if (includesAttributeUse) + { + Assert.NotNull(attribute); + Assert.Equal(type, attribute.AttributeClass); + Assert.Equal([2], attribute.ConstructorArguments.Select(a => a.Value)); + Assert.Equal([], attribute.NamedArguments); + } + else + { + Assert.Null(attribute); + } + } + + /// + /// (and ) should be symbol names () + /// or symbol getters (]]>) of symbols that are expected to be unsafe (or safe, respectively). + /// + private static void VerifyRequiresUnsafeAttribute( + ModuleSymbol module, + ReadOnlySpan expectedUnsafeSymbols, + ReadOnlySpan expectedSafeSymbols, + object[]? expectedNoAttributeInSource = null, + object[]? expectedNoAttribute = null, + object[]? expectedAttribute = null, + CallerUnsafeMode expectedUnsafeMode = CallerUnsafeMode.Explicit) + { + const string Name = "RequiresUnsafeAttribute"; + + var expectedNoAttributeInSourceSymbols = (expectedNoAttributeInSource ?? []).SelectAsArray(getSymbol); + var expectedNoAttributeSymbols = (expectedNoAttribute ?? []).SelectAsArray(getSymbol); + var expectedAttributeSymbols = (expectedAttribute ?? []).SelectAsArray(getSymbol); + + var seenSymbols = new HashSet(); + + foreach (var symbol in expectedUnsafeSymbols) + { + verifySymbol(symbol, shouldBeUnsafe: true); + } + + foreach (var symbol in expectedSafeSymbols) + { + verifySymbol(symbol, shouldBeUnsafe: false); + } + + Assert.All(expectedNoAttributeInSourceSymbols, s => Assert.True(seenSymbols.Contains(s))); + Assert.All(expectedNoAttributeSymbols, s => Assert.True(seenSymbols.Contains(s))); + Assert.All(expectedAttributeSymbols, s => Assert.True(seenSymbols.Contains(s))); + + void verifySymbol(object symbolGetter, bool shouldBeUnsafe) + { + var symbol = getSymbol(symbolGetter); + + var symbolExpectedUnsafeMode = shouldBeUnsafe ? expectedUnsafeMode : CallerUnsafeMode.None; + Assert.True(symbolExpectedUnsafeMode == symbol.CallerUnsafeMode, $"Expected {symbol.GetType().Name} '{symbol.ToTestDisplayString()}' to have {nameof(CallerUnsafeMode)}.{symbolExpectedUnsafeMode} (got {symbol.CallerUnsafeMode})."); + + var attribute = symbol.GetAttributes().SingleOrDefault(a => a.AttributeClass?.Name == Name); + var associatedAttribute = (symbol as MethodSymbol)?.AssociatedSymbol?.GetAttributes().SingleOrDefault(a => a.AttributeClass?.Name == Name); + var hasAttribute = attribute is not null || associatedAttribute is not null; + var shouldHaveAttribute = expectedAttributeSymbols.Contains(symbol) || + (shouldBeUnsafe && expectedUnsafeMode != CallerUnsafeMode.Implicit && + (module is not SourceModuleSymbol || !expectedNoAttributeInSourceSymbols.Contains(symbol)) && + !expectedNoAttributeSymbols.Contains(symbol)); + Assert.True(shouldHaveAttribute == hasAttribute, + $"{(shouldHaveAttribute ? "Expected" : "Did not expect")} {symbol.GetType().Name} '{symbol.ToTestDisplayString()}' or its associated symbol to have the attribute."); + + Assert.True(seenSymbols.Add(symbol), $"Symbol '{symbol.ToTestDisplayString()}' specified multiple times."); + + Assert.True(shouldBeUnsafe || !expectedNoAttributeInSourceSymbols.Contains(symbol), + $"Unexpected safe '{symbol.ToTestDisplayString()}' in {nameof(expectedNoAttributeInSource)}."); + } + + Symbol getSymbol(object symbolGetter) + { + var symbol = symbolGetter switch + { + string symbolName => module.GlobalNamespace.GetMember(symbolName), + Func func => func(module), + _ => throw ExceptionUtilities.UnexpectedValue(symbolGetter), + }; + Assert.False(symbol is null, $"Cannot find symbol '{symbolGetter}' in {module.GetType().Name}."); + return symbol; + } + } + + [Fact] + public void RulesAttribute_Synthesized() + { + var source = """ + class C; + """; + + CompileAndVerify(source, + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: false, includesAttributeUse: false)) + .VerifyDiagnostics(); + + var ref1 = CompileAndVerify(source, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: true, isSynthesized: true)) + .VerifyDiagnostics() + .GetImageReference(); + + CompileAndVerify("", [ref1], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: true, isSynthesized: true)) + .VerifyDiagnostics(); + + var source2 = """ + class B; + """; + + CompileAndVerify(source2, [ref1], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: true, isSynthesized: true)) + .VerifyDiagnostics(); + + CompileAndVerify(source, + options: TestOptions.ReleaseModule, + verify: Verification.Skipped, + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: false, includesAttributeUse: false)) + .VerifyDiagnostics(); + + CreateCompilation(source, + options: TestOptions.ReleaseModule.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,1): error CS0518: Predefined type 'System.Runtime.CompilerServices.MemorySafetyRulesAttribute' is not defined or imported + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound).WithArguments("System.Runtime.CompilerServices.MemorySafetyRulesAttribute").WithLocation(1, 1)); + + // Script compilation. + source = "System.Console.WriteLine();"; + + CompileAndVerify(source, + parseOptions: TestOptions.Script, + options: TestOptions.ReleaseModule, + verify: Verification.Skipped, + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: false, includesAttributeUse: false)) + .VerifyDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Script, + options: TestOptions.ReleaseModule.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // error CS0518: Predefined type 'System.Runtime.CompilerServices.MemorySafetyRulesAttribute' is not defined or imported + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound).WithArguments("System.Runtime.CompilerServices.MemorySafetyRulesAttribute").WithLocation(1, 1)); + + // No types and members in the compilation, but the attribute is still synthesized if updated rules are enabled. + source = """ + [assembly: System.Reflection.AssemblyDescriptionAttribute(null)] + """; + + CompileAndVerify(source, + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: false, includesAttributeUse: false)) + .VerifyDiagnostics(); + + CompileAndVerify(source, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: true, isSynthesized: true)) + .VerifyDiagnostics(); + + CreateCompilation(source, + options: TestOptions.ReleaseModule.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,1): error CS0518: Predefined type 'System.Runtime.CompilerServices.MemorySafetyRulesAttribute' is not defined or imported + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound).WithArguments("System.Runtime.CompilerServices.MemorySafetyRulesAttribute").WithLocation(1, 1)); + } + + [Theory, CombinatorialData] + public void RulesAttribute_TypeForwardedTo( + bool updatedRulesA, + bool updatedRulesB, + bool useCompilationReference) + { + var sourceA = """ + public class A { } + """; + var comp = CreateCompilation(sourceA, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(updatedRulesA)) + .VerifyDiagnostics(); + var refA = AsReference(comp, useCompilationReference); + Assert.Equal(updatedRulesA, comp.SourceModule.UseUpdatedMemorySafetyRules); + CompileAndVerify(comp, + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: updatedRulesA, includesAttributeUse: updatedRulesA, isSynthesized: updatedRulesA ? true : null)) + .VerifyDiagnostics(); + + var sourceB = """ + using System.Runtime.CompilerServices; + [assembly: TypeForwardedTo(typeof(A))] + """; + comp = CreateCompilation(sourceB, [refA], options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(updatedRulesB)); + Assert.Equal(updatedRulesB, comp.SourceModule.UseUpdatedMemorySafetyRules); + CompileAndVerify(comp, + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: updatedRulesB, includesAttributeUse: updatedRulesB, isSynthesized: updatedRulesB ? true : null)) + .VerifyDiagnostics(); + } + + [Fact] + public void RulesAttribute_Reflection() + { + var sourceA = """ + using System; + using System.Linq; + public class A + { + public static int GetAttributeValue(Type type) + { + var module = type.Assembly.Modules.Single(); + var attribute = module.GetCustomAttributes(inherit: false).Single(a => a.GetType().Name == "MemorySafetyRulesAttribute"); + var prop = attribute.GetType().GetProperty("Version"); + return (int)prop.GetValue(attribute); + } + } + """; + var refA = CreateCompilation(sourceA, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics() + .EmitToImageReference(); + + var sourceB = """ + using System; + class B : A + { + static void Main() + { + Console.Write(GetAttributeValue(typeof(A))); + Console.Write(" "); + Console.Write(GetAttributeValue(typeof(B))); + } + } + """; + CompileAndVerify(sourceB, [refA], + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules(), + expectedOutput: "2 2") + .VerifyDiagnostics(); + } + + [Fact] + public void RulesAttribute_FromSource() + { + var source = """ + class C; + """; + + CompileAndVerify([source, MemorySafetyRulesAttributeDefinition], + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: false, isSynthesized: false)) + .VerifyDiagnostics(); + + CompileAndVerify([source, MemorySafetyRulesAttributeDefinition], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: true, isSynthesized: false)) + .VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void RulesAttribute_FromMetadata(bool useCompilationReference) + { + var comp = CreateCompilation(MemorySafetyRulesAttributeDefinition); + CompileAndVerify(comp, + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: false, isSynthesized: false)) + .VerifyDiagnostics(); + var ref1 = AsReference(comp, useCompilationReference); + + var source = """ + class C; + """; + + CompileAndVerify(source, [ref1], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: false, includesAttributeUse: true, isSynthesized: false)) + .VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void RulesAttribute_FromMetadata_Multiple(bool useCompilationReference) + { + var comp1 = CreateCompilation(MemorySafetyRulesAttributeDefinition).VerifyDiagnostics(); + var ref1 = AsReference(comp1, useCompilationReference); + + var comp2 = CreateCompilation(MemorySafetyRulesAttributeDefinition).VerifyDiagnostics(); + var ref2 = AsReference(comp2, useCompilationReference); + + var source = """ + class C; + """; + + // Ambiguous attribute definitions from references => synthesize our own. + CompileAndVerify(source, [ref1, ref2], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: true, isSynthesized: true)) + .VerifyDiagnostics(); + + // Also defined in source. + CompileAndVerify([source, MemorySafetyRulesAttributeDefinition], [ref1, ref2], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: true, isSynthesized: false)) + .VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void RulesAttribute_FromMetadata_Multiple_AndCorLib(bool useCompilationReference) + { + var corlibSource = """ + namespace System + { + public class Object; + public class ValueType; + public class Attribute; + public struct Void; + public struct Int32; + public struct Boolean; + public class AttributeUsageAttribute : Attribute + { + public AttributeUsageAttribute(AttributeTargets t) { } + public bool AllowMultiple { get; set; } + public bool Inherited { get; set; } + } + public class Enum; + public enum AttributeTargets + { + Module = 2 + } + } + """; + + var corlib = CreateEmptyCompilation([corlibSource, MemorySafetyRulesAttributeDefinition]).VerifyDiagnostics(); + var corlibRef = AsReference(corlib, useCompilationReference); + + var comp1 = CreateEmptyCompilation(MemorySafetyRulesAttributeDefinition, [corlibRef]).VerifyDiagnostics(); + var ref1 = AsReference(comp1, useCompilationReference); + + var comp2 = CreateEmptyCompilation(MemorySafetyRulesAttributeDefinition, [corlibRef]).VerifyDiagnostics(); + var ref2 = AsReference(comp2, useCompilationReference); + + var source = """ + class C; + """; + + // Using the attribute from corlib even if there are ambiguous definitions in other references. + var verifier = CompileAndVerify(CreateEmptyCompilation(source, [ref1, ref2, corlibRef], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()), + verify: Verification.Skipped, + symbolValidator: m => VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: false, includesAttributeUse: true, isSynthesized: false)); + + verifier.Diagnostics.WhereAsArray(d => d.Code != (int)ErrorCode.WRN_NoRuntimeMetadataVersion).Verify(); + + var comp = (CSharpCompilation)verifier.Compilation; + Assert.Same(comp.Assembly.CorLibrary, comp.GetReferencedAssemblySymbol(corlibRef)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + [InlineData(1)] + [InlineData(2, true)] + [InlineData(3)] + [InlineData(int.MinValue)] + [InlineData(int.MaxValue)] + public void RulesAttribute_FromMetadata_Version(int version, bool correctVersion = false) + { + // [module: MemorySafetyRules({version})] + // public class A { public static void M() => throw null; } + var sourceA = $$""" + .assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) } + .assembly '<>' { } + .module '<>.dll' + .custom instance void System.Runtime.CompilerServices.MemorySafetyRulesAttribute::.ctor(int32) = { int32({{version}}) } + .class private System.Runtime.CompilerServices.MemorySafetyRulesAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(int32 version) cil managed { ret } + } + .class public A + { + .method public static void M() { ldnull throw } + } + """; + var refA = CompileIL(sourceA, prependDefaultHeader: false); + + var sourceB = """ + class B + { + void M() => A.M(); + } + """; + var comp = CreateCompilation(sourceB, [refA]); + if (correctVersion) + { + comp.VerifyEmitDiagnostics(); + } + else + { + comp.VerifyDiagnostics( + // (3,17): error CS9103: 'A.M()' is defined in a module with an unrecognized System.Runtime.CompilerServices.MemorySafetyRulesAttribute version, expecting '2'. + // void M() => A.M(); + Diagnostic(ErrorCode.ERR_UnrecognizedAttributeVersion, "A.M").WithArguments("A.M()", "System.Runtime.CompilerServices.MemorySafetyRulesAttribute", "2").WithLocation(3, 17)); + } + + var method = comp.GetMember("B.M"); + Assert.False(method.ContainingModule.UseUpdatedMemorySafetyRules); + + // 'A.M' not used => no error. + CreateCompilation("class C;", references: [refA]).VerifyEmitDiagnostics(); + } + + [Theory] + [InlineData(2, 0, true)] + [InlineData(0, 2, false)] + public void RulesAttribute_FromMetadata_Version_Multiple(int version1, int version2, bool correctVersion) + { + // [module: MemorySafetyRules({version1})] + // [module: MemorySafetyRules({version2})] + // public class A { public static void M() => throw null; } + var sourceA = $$""" + .assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) } + .assembly '<>' { } + .module '<>.dll' + .custom instance void System.Runtime.CompilerServices.MemorySafetyRulesAttribute::.ctor(int32) = { int32({{version1}}) } + .custom instance void System.Runtime.CompilerServices.MemorySafetyRulesAttribute::.ctor(int32) = { int32({{version2}}) } + .class private System.Runtime.CompilerServices.MemorySafetyRulesAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(int32 version) cil managed { ret } + } + .class public A + { + .method public static void M() { ldnull throw } + } + """; + var refA = CompileIL(sourceA, prependDefaultHeader: false); + + var a = CreateCompilation("", [refA]).GetReferencedAssemblySymbol(refA); + Assert.Equal(correctVersion, a.Modules.Single().UseUpdatedMemorySafetyRules); + + var sourceB = """ + class B + { + void M() => A.M(); + } + """; + var comp = CreateCompilation(sourceB, [refA]); + if (correctVersion) + { + comp.VerifyEmitDiagnostics(); + } + else + { + comp.VerifyDiagnostics( + // (3,17): error CS9103: 'A.M()' is defined in a module with an unrecognized System.Runtime.CompilerServices.MemorySafetyRulesAttribute version, expecting '2'. + // void M() => A.M(); + Diagnostic(ErrorCode.ERR_UnrecognizedAttributeVersion, "A.M").WithArguments("A.M()", "System.Runtime.CompilerServices.MemorySafetyRulesAttribute", "2").WithLocation(3, 17)); + } + + var method = comp.GetMember("B.M"); + Assert.False(method.ContainingModule.UseUpdatedMemorySafetyRules); + + // 'A.M' not used => no error. + CreateCompilation("class C;", references: [refA]).VerifyEmitDiagnostics(); + } + + [Fact] + public void RulesAttribute_FromMetadata_UnrecognizedConstructor_NoArguments() + { + // [module: MemorySafetyRules()] + // public class A { public static void M() => throw null; } + var sourceA = """ + .assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) } + .assembly '<>' { } + .module '<>.dll' + .custom instance void System.Runtime.CompilerServices.MemorySafetyRulesAttribute::.ctor() = ( 01 00 00 00 ) + .class private System.Runtime.CompilerServices.MemorySafetyRulesAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ret } + } + .class public A + { + .method public static void M() { ldnull throw } + } + """; + var refA = CompileIL(sourceA, prependDefaultHeader: false); + + var sourceB = """ + class B + { + void M() => A.M(); + } + """; + var comp = CreateCompilation(sourceB, [refA]); + comp.VerifyDiagnostics( + // (3,17): error CS9103: 'A.M()' is defined in a module with an unrecognized System.Runtime.CompilerServices.MemorySafetyRulesAttribute version, expecting '2'. + // void M() => A.M(); + Diagnostic(ErrorCode.ERR_UnrecognizedAttributeVersion, "A.M").WithArguments("A.M()", "System.Runtime.CompilerServices.MemorySafetyRulesAttribute", "2").WithLocation(3, 17)); + + var method = comp.GetMember("B.M"); + Assert.False(method.ContainingModule.UseUpdatedMemorySafetyRules); + + // 'A.M' not used => no error. + CreateCompilation("class C;", references: [refA]).VerifyEmitDiagnostics(); + } + + [Fact] + public void RulesAttribute_FromMetadata_UnrecognizedConstructor_StringArgument() + { + // [module: MemorySafetyRules("2")] + // public class A { public static void M() => throw null; } + var sourceA = """ + .assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) } + .assembly '<>' { } + .module '<>.dll' + .custom instance void System.Runtime.CompilerServices.MemorySafetyRulesAttribute::.ctor(string) = {string('2')} + .class private System.Runtime.CompilerServices.MemorySafetyRulesAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(string version) cil managed { ret } + } + .class public A + { + .method public static void M() { ldnull throw } + } + """; + var refA = CompileIL(sourceA, prependDefaultHeader: false); + + var sourceB = """ + class B + { + void M() => A.M(); + } + """; + var comp = CreateCompilation(sourceB, [refA]); + comp.VerifyDiagnostics( + // (3,17): error CS9103: 'A.M()' is defined in a module with an unrecognized System.Runtime.CompilerServices.MemorySafetyRulesAttribute version, expecting '2'. + // void M() => A.M(); + Diagnostic(ErrorCode.ERR_UnrecognizedAttributeVersion, "A.M").WithArguments("A.M()", "System.Runtime.CompilerServices.MemorySafetyRulesAttribute", "2").WithLocation(3, 17)); + + var method = comp.GetMember("B.M"); + Assert.False(method.ContainingModule.UseUpdatedMemorySafetyRules); + + // 'A.M' not used => no error. + CreateCompilation("class C;", references: [refA]).VerifyEmitDiagnostics(); + } + + [Fact] + public void RulesAttribute_MissingConstructor() + { + var source1 = """ + namespace System.Runtime.CompilerServices + { + public sealed class MemorySafetyRulesAttribute : Attribute { } + } + """; + var source2 = """ + class C; + """; + + CreateCompilation([source1, source2]).VerifyEmitDiagnostics(); + + CreateCompilation([source1, source2], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics() + .VerifyEmitDiagnostics( + // error CS0656: Missing compiler required member 'System.Runtime.CompilerServices.MemorySafetyRulesAttribute..ctor' + Diagnostic(ErrorCode.ERR_MissingPredefinedMember).WithArguments("System.Runtime.CompilerServices.MemorySafetyRulesAttribute", ".ctor").WithLocation(1, 1)); + + CreateCompilation([source1, source2], + options: TestOptions.ReleaseModule.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,1): error CS0656: Missing compiler required member 'System.Runtime.CompilerServices.MemorySafetyRulesAttribute..ctor' + Diagnostic(ErrorCode.ERR_MissingPredefinedMember).WithArguments("System.Runtime.CompilerServices.MemorySafetyRulesAttribute", ".ctor").WithLocation(1, 1)); + } + + [Theory, CombinatorialData] + public void RulesAttribute_ReferencedInSource( + bool updatedRules, + bool useCompilationReference) + { + var comp = CreateCompilation(MemorySafetyRulesAttributeDefinition).VerifyDiagnostics(); + var ref1 = AsReference(comp, useCompilationReference); + + var source = """ + using System.Runtime.CompilerServices; + [assembly: MemorySafetyRules(2)] + [module: MemorySafetyRules(2)] + """; + + comp = CreateCompilation(source, [ref1], options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(updatedRules)); + comp.VerifyDiagnostics( + // (2,12): error CS0592: Attribute 'MemorySafetyRules' is not valid on this declaration type. It is only valid on 'module' declarations. + // [assembly: MemorySafetyRules(2)] + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "MemorySafetyRules").WithArguments("MemorySafetyRules", "module").WithLocation(2, 12), + // (3,10): error CS8335: Do not use 'System.Runtime.CompilerServices.MemorySafetyRulesAttribute'. This is reserved for compiler usage. + // [module: MemorySafetyRules(2)] + Diagnostic(ErrorCode.ERR_ExplicitReservedAttr, "MemorySafetyRules(2)").WithArguments("System.Runtime.CompilerServices.MemorySafetyRulesAttribute").WithLocation(3, 10)); + } + + [Theory, CombinatorialData] + public void Pointer_Variable_SafeContext(bool allowUnsafe) + { + var source = """ + int* x = null; + """; + + var expectedDiagnostics = new[] + { + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 1), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe)).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe)).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe)).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe).WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe).WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe).WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_Variable_SafeContext_Var() + { + var source = """ + var x = GetPointer(); + int* GetPointer() => null; + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,9): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // var x = GetPointer(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "GetPointer()").WithArguments("updated memory safety rules").WithLocation(1, 9), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* GetPointer() => null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(2, 1)); + } + + [Fact] + public void Pointer_Variable_SafeContext_InIterator() + { + var source = """ + unsafe + { + M(); + System.Collections.Generic.IEnumerable M() + { + int* p = null; + yield return 1; + } + } + """; + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe).VerifyDiagnostics( + // (6,9): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(6, 9)); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (6,9): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(6, 9)); + + var expectedDiagnostics = new[] + { + // (6,9): error CS9202: Feature 'ref and unsafe in async and iterator methods' is not available in C# 12.0. Please use language version 13.0 or greater. + // int* p = null; + Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion12, "int*").WithArguments("ref and unsafe in async and iterator methods", "13.0").WithLocation(6, 9), + }; + + CreateCompilation(source, + parseOptions: TestOptions.Regular12, + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular12, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + [ + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 12.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "12.0", "preview").WithLocation(1, 1), + .. expectedDiagnostics, + ]); + } + + [Fact] + public void Pointer_Variable_UnsafeContext() + { + var source = """ + unsafe { int* x = null; } + """; + + var expectedDiagnostics = new[] + { + // (1,1): error CS0227: Unsafe code may only appear if compiling with /unsafe + // unsafe { int* x = null; } + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "unsafe").WithLocation(1, 1), + }; + + CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe).VerifyEmitDiagnostics(); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_Variable_UsingAlias_SafeContext() + { + var source = """ + using X = int*; + X x = null; + """; + + var expectedDiagnostics = new[] + { + // (1,11): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // using X = int*; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 11), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // X x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "X").WithLocation(2, 1), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,11): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // using X = int*; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 11), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // X x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "X").WithArguments("updated memory safety rules").WithLocation(2, 1)); + } + + [Fact] + public void Pointer_Variable_UsingAlias_UnsafeContext() + { + var source = """ + using unsafe X = int*; + unsafe { X x = null; } + """; + + var expectedDiagnostics = new[] + { + // (1,7): error CS0227: Unsafe code may only appear if compiling with /unsafe + // using unsafe X = int*; + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "unsafe").WithLocation(1, 7), + // (2,1): error CS0227: Unsafe code may only appear if compiling with /unsafe + // unsafe { X x = null; } + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "unsafe").WithLocation(2, 1), + }; + + CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe).VerifyEmitDiagnostics(); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1)); + } + + [Theory, CombinatorialData] + public void Pointer_Dereference_SafeContext(bool allowUnsafe) + { + var source = """ + int* x = null; + int y = *x; + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe)) + .VerifyDiagnostics( + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 1), + // (2,10): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int y = *x; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(2, 10)); + + var expectedDiagnostics = new[] + { + // (2,9): error CS9360: This operation may only be used in an unsafe context + // int y = *x; + Diagnostic(ErrorCode.ERR_UnsafeOperation, "*").WithLocation(2, 9), + }; + + CreateCompilation(source, + options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe).WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe).WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithAllowUnsafe(allowUnsafe).WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (2,10): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int y = *x; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(2, 10), + // (2,9): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int y = *x; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "*").WithArguments("updated memory safety rules").WithLocation(2, 9)); + } + + [Fact] + public void Pointer_Dereference_SafeContext_InIterator() + { + var source = """ + unsafe + { + M(); + System.Collections.Generic.IEnumerable M() + { + int* p = null; + int y = *p; + yield return 1; + } + } + """; + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe).VerifyDiagnostics( + // (6,9): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(6, 9), + // (7,18): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int y = *p; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "p").WithLocation(7, 18)); + + var expectedDiagnostics = new[] + { + // (7,17): error CS9360: This operation may only be used in an unsafe context + // int y = *p; + Diagnostic(ErrorCode.ERR_UnsafeOperation, "*").WithLocation(7, 17), + }; + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (6,9): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(6, 9), + // (7,18): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int y = *p; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "p").WithArguments("updated memory safety rules").WithLocation(7, 18), + // (7,17): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int y = *p; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "*").WithArguments("updated memory safety rules").WithLocation(7, 17)); + + expectedDiagnostics = + [ + // (6,9): error CS9202: Feature 'ref and unsafe in async and iterator methods' is not available in C# 12.0. Please use language version 13.0 or greater. + // int* p = null; + Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion12, "int*").WithArguments("ref and unsafe in async and iterator methods", "13.0").WithLocation(6, 9), + // (7,18): error CS9202: Feature 'ref and unsafe in async and iterator methods' is not available in C# 12.0. Please use language version 13.0 or greater. + // int y = *p; + Diagnostic(ErrorCode.ERR_FeatureNotAvailableInVersion12, "p").WithArguments("ref and unsafe in async and iterator methods", "13.0").WithLocation(7, 18), + ]; + + CreateCompilation(source, + parseOptions: TestOptions.Regular12, + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular12, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + [ + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 12.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "12.0", "preview").WithLocation(1, 1), + .. expectedDiagnostics, + ]); + } + + [Fact] + public void Pointer_Dereference_SafeContext_Null() + { + var source = """ + int x = *null; + """; + + var expectedDiagnostics = new[] + { + // (1,9): error CS0193: The * or -> operator must be applied to a pointer + // int x = *null; + Diagnostic(ErrorCode.ERR_PtrExpected, "*null").WithLocation(1, 9), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics([ + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + .. expectedDiagnostics, + ]); + } + + [Fact] + public void Pointer_Dereference_UnsafeContext() + { + var source = """ + int* x = null; + unsafe { int y = *x; } + """; + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_MemberAccess_SafeContext() + { + var source = """ + int* x = null; + string s = x->ToString(); + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 1), + // (2,12): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // string s = x->ToString(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(2, 12)); + + var expectedDiagnostics = new[] + { + // (2,13): error CS9360: This operation may only be used in an unsafe context + // string s = x->ToString(); + Diagnostic(ErrorCode.ERR_UnsafeOperation, "->").WithLocation(2, 13), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (2,12): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // string s = x->ToString(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(2, 12), + // (2,13): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // string s = x->ToString(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "->").WithArguments("updated memory safety rules").WithLocation(2, 13)); + } + + [Fact] + public void Pointer_MemberAccess_SafeContext_Null() + { + var source = """ + string s = null->ToString(); + """; + + var expectedDiagnostics = new[] + { + // (1,12): error CS0193: The * or -> operator must be applied to a pointer + // string s = null->ToString(); + Diagnostic(ErrorCode.ERR_PtrExpected, "null->ToString").WithLocation(1, 12), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics([ + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + .. expectedDiagnostics, + ]); + } + + [Fact] + public void Pointer_MemberAccess_UnsafeContext() + { + var source = """ + int* x = null; + unsafe { string s = x->ToString(); } + """; + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_MemberAccessViaDereference_SafeContext() + { + var source = """ + int* x = null; + string s = (*x).ToString(); + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 1), + // (2,14): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // string s = (*x).ToString(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(2, 14)); + + var expectedDiagnostics = new[] + { + // (2,13): error CS9360: This operation may only be used in an unsafe context + // string s = (*x).ToString(); + Diagnostic(ErrorCode.ERR_UnsafeOperation, "*").WithLocation(2, 13), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (2,14): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // string s = (*x).ToString(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(2, 14), + // (2,13): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // string s = (*x).ToString(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "*").WithArguments("updated memory safety rules").WithLocation(2, 13)); + } + + [Fact] + public void Pointer_MemberAccessViaDereference_UnsafeContext() + { + var source = """ + int* x = null; + unsafe { string s = (*x).ToString(); } + """; + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_ElementAccess_SafeContext() + { + var source = """ + int* x = null; + x[0] = 1; + int y = x[1]; + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // x[0] = 1; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(2, 1), + // (3,9): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int y = x[1]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(3, 9)); + + var expectedDiagnostics = new[] + { + // (2,2): error CS9360: This operation may only be used in an unsafe context + // x[0] = 1; + Diagnostic(ErrorCode.ERR_UnsafeOperation, "[").WithLocation(2, 2), + // (3,10): error CS9360: This operation may only be used in an unsafe context + // int y = x[1]; + Diagnostic(ErrorCode.ERR_UnsafeOperation, "[").WithLocation(3, 10), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // x[0] = 1; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,2): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // x[0] = 1; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "[").WithArguments("updated memory safety rules").WithLocation(2, 2), + // (3,9): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int y = x[1]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(3, 9), + // (3,10): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int y = x[1]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "[").WithArguments("updated memory safety rules").WithLocation(3, 10)); + } + + [Fact] + public void Pointer_ElementAccess_SafeContext_MultipleIndices() + { + var source = """ + int* x = null; + x[0, 1] = 1; + int y = x[2, 3]; + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // x[0, 1] = 1; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(2, 1), + // (2,1): error CS0196: A pointer must be indexed by only one value + // x[0, 1] = 1; + Diagnostic(ErrorCode.ERR_PtrIndexSingle, "x[0, 1]").WithLocation(2, 1), + // (3,9): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int y = x[2, 3]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(3, 9), + // (3,9): error CS0196: A pointer must be indexed by only one value + // int y = x[2, 3]; + Diagnostic(ErrorCode.ERR_PtrIndexSingle, "x[2, 3]").WithLocation(3, 9)); + + var expectedDiagnostics = new[] + { + // (2,1): error CS0196: A pointer must be indexed by only one value + // x[0, 1] = 1; + Diagnostic(ErrorCode.ERR_PtrIndexSingle, "x[0, 1]").WithLocation(2, 1), + // (3,9): error CS0196: A pointer must be indexed by only one value + // int y = x[2, 3]; + Diagnostic(ErrorCode.ERR_PtrIndexSingle, "x[2, 3]").WithLocation(3, 9), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // x[0, 1] = 1; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,1): error CS0196: A pointer must be indexed by only one value + // x[0, 1] = 1; + Diagnostic(ErrorCode.ERR_PtrIndexSingle, "x[0, 1]").WithLocation(2, 1), + // (3,9): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int y = x[2, 3]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(3, 9), + // (3,9): error CS0196: A pointer must be indexed by only one value + // int y = x[2, 3]; + Diagnostic(ErrorCode.ERR_PtrIndexSingle, "x[2, 3]").WithLocation(3, 9)); + } + + [Fact] + public void Pointer_ElementAccess_SafeContext_ArrayOfPointers() + { + var source = """ + int*[] x = []; + x[0] = null; + _ = x[1]; + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int*[] x = []; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // x[0] = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(2, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // x[0] = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x[0]").WithLocation(2, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // x[0] = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x[0] = null").WithLocation(2, 1), + // (3,5): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // _ = x[1]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(3, 5), + // (3,5): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // _ = x[1]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x[1]").WithLocation(3, 5), + // (3,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // _ = x[1]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "_ = x[1]").WithLocation(3, 1)); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int*[] x = []; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // x[0] = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // x[0] = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x[0]").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // x[0] = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x[0] = null").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (3,5): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = x[1]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(3, 5), + // (3,5): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = x[1]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x[1]").WithArguments("updated memory safety rules").WithLocation(3, 5), + // (3,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = x[1]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "_ = x[1]").WithArguments("updated memory safety rules").WithLocation(3, 1)); + } + + [Fact] + public void Pointer_ElementAccess_SafeContext_FunctionPointer() + { + var source = """ + delegate* x = null; + x[0] = null; + _ = x[1]; + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // delegate* x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "delegate*").WithLocation(1, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // x[0] = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(2, 1), + // (2,1): error CS0021: Cannot apply indexing with [] to an expression of type 'delegate*' + // x[0] = null; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "x[0]").WithArguments("delegate*").WithLocation(2, 1), + // (3,5): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // _ = x[1]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x").WithLocation(3, 5), + // (3,5): error CS0021: Cannot apply indexing with [] to an expression of type 'delegate*' + // _ = x[1]; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "x[1]").WithArguments("delegate*").WithLocation(3, 5)); + + var expectedDiagnostics = new[] + { + // (2,1): error CS0021: Cannot apply indexing with [] to an expression of type 'delegate*' + // x[0] = null; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "x[0]").WithArguments("delegate*").WithLocation(2, 1), + // (3,5): error CS0021: Cannot apply indexing with [] to an expression of type 'delegate*' + // _ = x[1]; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "x[1]").WithArguments("delegate*").WithLocation(3, 5), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // delegate* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "delegate*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // x[0] = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,1): error CS0021: Cannot apply indexing with [] to an expression of type 'delegate*' + // x[0] = null; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "x[0]").WithArguments("delegate*").WithLocation(2, 1), + // (3,5): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = x[1]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x").WithArguments("updated memory safety rules").WithLocation(3, 5), + // (3,5): error CS0021: Cannot apply indexing with [] to an expression of type 'delegate*' + // _ = x[1]; + Diagnostic(ErrorCode.ERR_BadIndexLHS, "x[1]").WithArguments("delegate*").WithLocation(3, 5)); + } + + [Fact] + public void Pointer_ElementAccess_UnsafeContext() + { + var source = """ + int* x = null; + unsafe + { + x[0] = 1; + int y = x[1]; + } + """; + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_Function_Variable_SafeContext() + { + var source = """ + delegate* f = null; + """; + + var expectedDiagnostics = new[] + { + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // delegate* f = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "delegate*").WithLocation(1, 1), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // delegate* f = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "delegate*").WithArguments("updated memory safety rules").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_Function_Variable_UnsafeContext() + { + var source = """ + unsafe { delegate* f = null; } + """; + + var expectedDiagnostics = new[] + { + // (1,1): error CS0227: Unsafe code may only appear if compiling with /unsafe + // unsafe { delegate* f = null; } + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "unsafe").WithLocation(1, 1), + }; + + CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe).VerifyEmitDiagnostics(); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_Function_Variable_UsingAlias_SafeContext() + { + var source = """ + using X = delegate*; + X x = null; + """; + + var expectedDiagnostics = new[] + { + // (1,11): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // using X = delegate*; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "delegate*").WithLocation(1, 11), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // X x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "X").WithLocation(2, 1), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + // https://github.com/dotnet/roslyn/issues/77389 + expectedDiagnostics = PlatformInformation.IsWindows + ? [ + // error CS8911: Using a function pointer type in this context is not supported. + Diagnostic(ErrorCode.ERR_FunctionPointerTypesInAttributeNotSupported).WithLocation(1, 1), + ] + : []; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics() + .VerifyEmitDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics() + .VerifyEmitDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,11): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // using X = delegate*; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "delegate*").WithArguments("updated memory safety rules").WithLocation(1, 11), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // X x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "X").WithArguments("updated memory safety rules").WithLocation(2, 1)); + } + + [Fact] + public void Pointer_Function_Variable_UsingAlias_UnsafeContext() + { + var source = """ + using unsafe X = delegate*; + unsafe { X x = null; } + """; + + var expectedDiagnostics = new[] + { + // (1,7): error CS0227: Unsafe code may only appear if compiling with /unsafe + // using unsafe X = delegate*; + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "unsafe").WithLocation(1, 7), + // (2,1): error CS0227: Unsafe code may only appear if compiling with /unsafe + // unsafe { X x = null; } + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "unsafe").WithLocation(2, 1), + }; + + CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + // https://github.com/dotnet/roslyn/issues/77389 + expectedDiagnostics = PlatformInformation.IsWindows + ? [ + // error CS8911: Using a function pointer type in this context is not supported. + Diagnostic(ErrorCode.ERR_FunctionPointerTypesInAttributeNotSupported).WithLocation(1, 1), + ] + : []; + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics() + .VerifyEmitDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics() + .VerifyEmitDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics() + .VerifyEmitDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_Function_Call_SafeContext() + { + var source = """ + delegate* x = null; + string s = x(); + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // delegate* x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "delegate*").WithLocation(1, 1), + // (2,12): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // string s = x(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x()").WithLocation(2, 12)); + + var expectedDiagnostics = new[] + { + // (2,12): error CS9360: This operation may only be used in an unsafe context + // string s = x(); + Diagnostic(ErrorCode.ERR_UnsafeOperation, "x()").WithLocation(2, 12), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // delegate* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "delegate*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (2,12): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // string s = x(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x()").WithArguments("updated memory safety rules").WithLocation(2, 12)); + } + + [Fact] + public void Pointer_Function_Call_UnsafeContext() + { + var source = """ + delegate* x = null; + unsafe { string s = x(); } + """; + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // delegate* x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "delegate*").WithArguments("updated memory safety rules").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_Function_Call_UsingAlias() + { + var source = """ + using X = delegate*; + X x = null; + string s = x(); + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (1,11): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // using X = delegate*; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "delegate*").WithLocation(1, 11), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // X x = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "X").WithLocation(2, 1), + // (3,12): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // string s = x(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x()").WithLocation(3, 12)); + + var expectedDiagnostics = new[] + { + // (3,12): error CS9360: This operation may only be used in an unsafe context + // string s = x(); + Diagnostic(ErrorCode.ERR_UnsafeOperation, "x()").WithLocation(3, 12), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,11): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // using X = delegate*; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "delegate*").WithArguments("updated memory safety rules").WithLocation(1, 11), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // X x = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "X").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (3,12): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // string s = x(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x()").WithArguments("updated memory safety rules").WithLocation(3, 12)); + } + + [Fact] + public void Pointer_AddressOf_SafeContext() + { + var source = """ + int x; + int* p = &x; + """; + + var expectedDiagnostics = new[] + { + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p = &x; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(2, 1), + // (2,10): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p = &x; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "&x").WithLocation(2, 10), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p = &x; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,10): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p = &x; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "&x").WithArguments("updated memory safety rules").WithLocation(2, 10)); + } + + [Fact] + public void Pointer_AddressOf_SafeContext_Const() + { + var source = """ + const int x = 1; + int* p = &x; + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p = &x; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(2, 1), + // (2,11): error CS0211: Cannot take the address of the given expression + // int* p = &x; + Diagnostic(ErrorCode.ERR_InvalidAddrOp, "x").WithLocation(2, 11)); + + var expectedDiagnostics = new[] + { + // (2,11): error CS0211: Cannot take the address of the given expression + // int* p = &x; + Diagnostic(ErrorCode.ERR_InvalidAddrOp, "x").WithLocation(2, 11), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p = &x; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,11): error CS0211: Cannot take the address of the given expression + // int* p = &x; + Diagnostic(ErrorCode.ERR_InvalidAddrOp, "x").WithLocation(2, 11)); + } + + [Fact] + public void Pointer_AddressOf_UnsafeContext() + { + var source = """ + int x; + unsafe { int* p = &x; } + """; + + var expectedDiagnostics = new[] + { + // (2,1): error CS0227: Unsafe code may only appear if compiling with /unsafe + // unsafe { int* p = &x; } + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "unsafe").WithLocation(2, 1), + }; + + CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe).VerifyEmitDiagnostics(); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_Fixed_SafeContext() + { + var source = """ + class C + { + static int x; + static void Main() + { + fixed (int* p = &x) { } + } + } + """; + + var expectedDiagnostics = new[] + { + // (6,9): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "fixed (int* p = &x) { }").WithLocation(6, 9), + // (6,16): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(6, 16), + // (6,25): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "&x").WithLocation(6, 25), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (6,9): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "fixed (int* p = &x) { }").WithArguments("updated memory safety rules").WithLocation(6, 9), + // (6,16): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(6, 16), + // (6,25): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "&x").WithArguments("updated memory safety rules").WithLocation(6, 25)); + } + + [Fact] + public void Pointer_Fixed_PatternBased() + { + var source = """ + class C + { + static void Main() + { + fixed (int* p = new S()) { } + } + } + + struct S + { + public ref readonly int GetPinnableReference() => throw null; + } + """; + + var expectedDiagnostics = new[] + { + // (5,9): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = new S()) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "fixed (int* p = new S()) { }").WithLocation(5, 9), + // (5,16): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = new S()) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(5, 16), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (5,9): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // fixed (int* p = new S()) { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "fixed (int* p = new S()) { }").WithArguments("updated memory safety rules").WithLocation(5, 9), + // (5,16): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // fixed (int* p = new S()) { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(5, 16)); + } + + [Fact] + public void Pointer_Fixed_SafeContext_AlreadyFixed() + { + var source = """ + int x; + fixed (int* p = &x) { } + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "fixed (int* p = &x) { }").WithLocation(2, 1), + // (2,8): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(2, 8), + // (2,17): error CS0213: You cannot use the fixed statement to take the address of an already fixed expression + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_FixedNotNeeded, "&x").WithLocation(2, 17)); + + var expectedDiagnostics = new[] + { + // (2,17): error CS0213: You cannot use the fixed statement to take the address of an already fixed expression + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_FixedNotNeeded, "&x").WithLocation(2, 17), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "fixed (int* p = &x) { }").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,8): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(2, 8), + // (2,17): error CS0213: You cannot use the fixed statement to take the address of an already fixed expression + // fixed (int* p = &x) { } + Diagnostic(ErrorCode.ERR_FixedNotNeeded, "&x").WithLocation(2, 17)); + } + + [Fact] + public void Pointer_Fixed_UnsafeContext() + { + var source = """ + class C + { + static int x; + static void Main() + { + unsafe { fixed (int* p = &x) { } } + } + } + """; + + var expectedDiagnostics = new[] + { + // (6,9): error CS0227: Unsafe code may only appear if compiling with /unsafe + // unsafe { fixed (int* p = &x) { } } + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "unsafe").WithLocation(6, 9), + }; + + CreateCompilation(source).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe).VerifyEmitDiagnostics(); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1)); + } + + [Fact] + public void Pointer_Arithmetic_SafeContext() + { + var source = """ + int* p = null; + p++; + int* p2 = p + 2; + long x = p - p; + bool b = p > p2; + """; + + var expectedDiagnostics = new[] + { + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p = null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // p++; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "p").WithLocation(2, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // p++; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "p++").WithLocation(2, 1), + // (3,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p2 = p + 2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(3, 1), + // (3,11): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p2 = p + 2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "p").WithLocation(3, 11), + // (3,11): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p2 = p + 2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "p + 2").WithLocation(3, 11), + // (4,10): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // long x = p - p; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "p").WithLocation(4, 10), + // (4,14): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // long x = p - p; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "p").WithLocation(4, 14), + // (5,10): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // bool b = p > p2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "p").WithLocation(5, 10), + // (5,14): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // bool b = p > p2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "p2").WithLocation(5, 14), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p = null; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // p++; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "p").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // p++; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "p++").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (3,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p2 = p + 2; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(3, 1), + // (3,11): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p2 = p + 2; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "p").WithArguments("updated memory safety rules").WithLocation(3, 11), + // (3,11): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p2 = p + 2; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "p + 2").WithArguments("updated memory safety rules").WithLocation(3, 11), + // (4,10): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // long x = p - p; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "p").WithArguments("updated memory safety rules").WithLocation(4, 10), + // (4,14): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // long x = p - p; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "p").WithArguments("updated memory safety rules").WithLocation(4, 14), + // (5,10): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // bool b = p > p2; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "p").WithArguments("updated memory safety rules").WithLocation(5, 10), + // (5,14): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // bool b = p > p2; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "p2").WithArguments("updated memory safety rules").WithLocation(5, 14)); + } + + [Fact] + public void SizeOf_SafeContext() + { + var source = """ + _ = sizeof(int); + _ = sizeof(nint); + _ = sizeof(S); + struct S; + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (2,5): error CS0233: 'nint' does not have a predefined size, therefore sizeof can only be used in an unsafe context + // _ = sizeof(nint); + Diagnostic(ErrorCode.ERR_SizeofUnsafe, "sizeof(nint)").WithArguments("nint").WithLocation(2, 5), + // (3,5): error CS0233: 'S' does not have a predefined size, therefore sizeof can only be used in an unsafe context + // _ = sizeof(S); + Diagnostic(ErrorCode.ERR_SizeofUnsafe, "sizeof(S)").WithArguments("S").WithLocation(3, 5)); + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()).VerifyEmitDiagnostics(); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (2,5): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = sizeof(nint); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "sizeof(nint)").WithArguments("updated memory safety rules").WithLocation(2, 5), + // (3,5): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // _ = sizeof(S); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "sizeof(S)").WithArguments("updated memory safety rules").WithLocation(3, 5)); + } + + [Fact] + public void FixedSizeBuffer_SafeContext() + { + var source = """ + var s = new S(); + int* p = s.y; + int z = s.x[100]; + + struct S + { + public fixed int x[5], y[10]; + } + """; + + CreateCompilation(source, options: TestOptions.ReleaseExe).VerifyDiagnostics( + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p = s.y; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(2, 1), + // (2,10): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* p = s.y; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "s.y").WithLocation(2, 10), + // (3,9): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int z = s.x[100]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "s.x").WithLocation(3, 9), + // (7,22): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public fixed int x[5], y[10]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "x[5]").WithLocation(7, 22)); + + var expectedDiagnostics = new[] + { + // (3,12): error CS9360: This operation may only be used in an unsafe context + // int z = s.x[100]; + Diagnostic(ErrorCode.ERR_UnsafeOperation, "[").WithLocation(3, 12), + }; + + CreateCompilation(source, options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p = s.y; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(2, 1), + // (2,10): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* p = s.y; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "s.y").WithArguments("updated memory safety rules").WithLocation(2, 10), + // (3,9): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int z = s.x[100]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "s.x").WithArguments("updated memory safety rules").WithLocation(3, 9), + // (3,12): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int z = s.x[100]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "[").WithArguments("updated memory safety rules").WithLocation(3, 12), + // (7,22): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public fixed int x[5], y[10]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "x[5]").WithArguments("updated memory safety rules").WithLocation(7, 22)); + } + + [Fact] + public void SkipLocalsInit_NeedsUnsafe() + { + var source = """ + class C { [System.Runtime.CompilerServices.SkipLocalsInit] void M() { } } + + namespace System.Runtime.CompilerServices + { + public class SkipLocalsInitAttribute : Attribute; + } + """; + + var expectedDiagnostics = new[] + { + // (1,12): error CS0227: Unsafe code may only appear if compiling with /unsafe + // class C { [System.Runtime.CompilerServices.SkipLocalsInit] void M() { } } + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "System.Runtime.CompilerServices.SkipLocalsInit").WithLocation(1, 12), + }; + + CreateCompilation(source, options: TestOptions.ReleaseDll) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseDll) + .VerifyEmitDiagnostics(); + + CreateCompilation(source, options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void StackAlloc_SafeContext() + { + var source = """ + int* x = stackalloc int[3]; + System.Span y = stackalloc int[5]; + M(); + + [System.Runtime.CompilerServices.SkipLocalsInit] + void M() + { + System.Span a = stackalloc int[5]; + System.Span b = stackalloc int[] { 1 }; + System.Span d = stackalloc int[2] { 1, 2 }; + System.Span e = stackalloc int[3] { 1, 2 }; + } + + namespace System.Runtime.CompilerServices + { + public class SkipLocalsInitAttribute : Attribute; + } + """; + + CreateCompilationWithSpan(source, options: TestOptions.UnsafeReleaseExe).VerifyDiagnostics( + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* x = stackalloc int[3]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 1), + // (1,10): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* x = stackalloc int[3]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "stackalloc int[3]").WithLocation(1, 10), + // (11,26): error CS0847: An array initializer of length '3' is expected + // System.Span e = stackalloc int[3] { 1, 2 }; + Diagnostic(ErrorCode.ERR_ArrayInitializerIncorrectLength, "stackalloc int[3] { 1, 2 }").WithArguments("3").WithLocation(11, 26)); + + var expectedDiagnostics = new[] + { + // (8,26): error CS9361: stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + // System.Span a = stackalloc int[5]; + Diagnostic(ErrorCode.ERR_UnsafeUninitializedStackAlloc, "stackalloc int[5]").WithLocation(8, 26), + // (11,26): error CS0847: An array initializer of length '3' is expected + // System.Span e = stackalloc int[3] { 1, 2 }; + Diagnostic(ErrorCode.ERR_ArrayInitializerIncorrectLength, "stackalloc int[3] { 1, 2 }").WithArguments("3").WithLocation(11, 26), + }; + + CreateCompilationWithSpan(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilationWithSpan(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilationWithSpan(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = stackalloc int[3]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "int*").WithArguments("updated memory safety rules").WithLocation(1, 1), + // (1,10): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // int* x = stackalloc int[3]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "stackalloc int[3]").WithArguments("updated memory safety rules").WithLocation(1, 10), + // (8,26): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // System.Span a = stackalloc int[5]; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "stackalloc int[5]").WithArguments("updated memory safety rules").WithLocation(8, 26), + // (11,26): error CS0847: An array initializer of length '3' is expected + // System.Span e = stackalloc int[3] { 1, 2 }; + Diagnostic(ErrorCode.ERR_ArrayInitializerIncorrectLength, "stackalloc int[3] { 1, 2 }").WithArguments("3").WithLocation(11, 26)); + } + + [Fact] + public void StackAlloc_UnsafeContext() + { + var source = """ + unsafe { System.Span y = stackalloc int[5]; } + M(); + + [System.Runtime.CompilerServices.SkipLocalsInit] + void M() + { + unsafe { System.Span a = stackalloc int[5]; } + unsafe { System.Span e = stackalloc int[3] { 1, 2 }; } + } + + namespace System.Runtime.CompilerServices + { + public class SkipLocalsInitAttribute : Attribute; + } + """; + + var expectedDiagnostics = new[] + { + // (8,35): error CS0847: An array initializer of length '3' is expected + // unsafe { System.Span e = stackalloc int[3] { 1, 2 }; } + Diagnostic(ErrorCode.ERR_ArrayInitializerIncorrectLength, "stackalloc int[3] { 1, 2 }").WithArguments("3").WithLocation(8, 35), + }; + + CreateCompilationWithSpan(source, options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilationWithSpan(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilationWithSpan(source, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilationWithSpan(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + [ + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + .. expectedDiagnostics, + ]); + } + + [Fact] + public void StackAlloc_Lambda() + { + var source = """ + var lam = [System.Runtime.CompilerServices.SkipLocalsInit] () => + { + System.Span a = stackalloc int[5]; + int* b = stackalloc int[3]; + unsafe { System.Span c = stackalloc int[1]; } + }; + + namespace System.Runtime.CompilerServices + { + public class SkipLocalsInitAttribute : Attribute; + } + """; + + CreateCompilationWithSpan(source, options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,26): error CS9361: stackalloc expression without an initializer inside SkipLocalsInit may only be used in an unsafe context + // System.Span a = stackalloc int[5]; + Diagnostic(ErrorCode.ERR_UnsafeUninitializedStackAlloc, "stackalloc int[5]").WithLocation(3, 26)); + } + + [Fact] + public void UnsafeDeclarations() + { + var source = """ + #pragma warning disable CS0169 // unused field + #pragma warning disable CS8019 // unused using + #pragma warning disable CS8321 // unused local function + using static unsafe System.Collections.Generic.List; + using unsafe U = int*; + unsafe void F() { } + unsafe class C; + unsafe struct S; + unsafe interface I; + unsafe enum E { A } + unsafe record R; + unsafe delegate void D(); + class X + { + unsafe X() { } + unsafe ~X() { } + unsafe int f; + unsafe void M() { } + unsafe int P { get; set; } + unsafe event System.Action E { add { } remove { } } + public static unsafe implicit operator int(X x) => 0; + } + """; + + var expectedDiagnostics = new[] + { + // (10,13): error CS0106: The modifier 'unsafe' is not valid for this item + // unsafe enum E { A } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "E").WithArguments("unsafe").WithLocation(10, 13), + }; + + CreateCompilation([source, IsExternalInitTypeDefinition], options: TestOptions.UnsafeReleaseExe).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation([source, IsExternalInitTypeDefinition], options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules().WithWarningLevel(10)).VerifyDiagnostics(expectedDiagnostics); + + expectedDiagnostics = + [ + // (10,13): error CS0106: The modifier 'unsafe' is not valid for this item + // unsafe enum E { A } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "E").WithArguments("unsafe").WithLocation(10, 13), + // (12,22): warning CS9377: The 'unsafe' modifier does not have any effect here under the current memory safety rules. + // unsafe delegate void D(); + Diagnostic(ErrorCode.WRN_UnsafeMeaningless, "D").WithLocation(12, 22), + ]; + + CreateCompilation([source, IsExternalInitTypeDefinition], options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation([source, IsExternalInitTypeDefinition], options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules().WithWarningLevel(11)).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation([source, IsExternalInitTypeDefinition], + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation([source, IsExternalInitTypeDefinition], + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()).VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (10,13): error CS0106: The modifier 'unsafe' is not valid for this item + // unsafe enum E { A } + Diagnostic(ErrorCode.ERR_BadMemberFlag, "E").WithArguments("unsafe").WithLocation(10, 13), + // (12,22): warning CS9377: The 'unsafe' modifier does not have any effect here under the current memory safety rules. + // unsafe delegate void D(); + Diagnostic(ErrorCode.WRN_UnsafeMeaningless, "D").WithLocation(12, 22)); + } + + [Fact] + public void Member_LangVersion() + { + CSharpTestSource source = + [ + """ + #pragma warning disable CS8321 // unused local function + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] void F() { } + class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] void M() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] int P { get; set; } + + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] event System.Action E { add { } remove { } } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] int this[int i] { get => i; set { } } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] C() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] public static C operator +(C c1, C c2) => c1; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] public void operator +=(C c) { } + #pragma warning disable CS0169 // unused field + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] int F; + } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] class U; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] delegate void D(); + """, + CompilerFeatureRequiredAttribute, + """ + namespace System.Diagnostics.CodeAnalysis + { + public sealed class RequiresUnsafeAttribute : Attribute; + } + """, + ]; + + string[] safeSymbols = ["C", "C.F", "U", "D"]; + string[] unsafeSymbols = + [ + "Program.<
$>g__F|0_0", + "C.M", + "C.P", "C.get_P", "C.set_P", + "C.E", "C.add_E", "C.remove_E", + "C.this[]", "C.get_Item", "C.set_Item", + "C..ctor", + "C.op_Addition", + "C.op_AdditionAssignment", + ]; + string[] symbolsWithAttribute = safeSymbols.Except(["C"]).Concat(unsafeSymbols).ToArray(); + + foreach (var parseOptions in new[] { TestOptions.RegularPreview, TestOptions.RegularNext, TestOptions.Regular14 }) + { + CompileAndVerify(source, + parseOptions: parseOptions, + options: TestOptions.UnsafeReleaseExe.WithMetadataImportOptions(MetadataImportOptions.All) + .WithSpecificDiagnosticOptions(GetIdForErrorCode(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules), ReportDiagnostic.Suppress), + symbolValidator: m => + { + VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: false, includesAttributeUse: false); + VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [], + expectedSafeSymbols: [.. safeSymbols, .. unsafeSymbols], + expectedAttribute: symbolsWithAttribute); + }) + .VerifyDiagnostics(); + } + + foreach (var parseOptions in new[] { TestOptions.RegularPreview, TestOptions.RegularNext }) + { + CompileAndVerify(source, + parseOptions: parseOptions, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules().WithMetadataImportOptions(MetadataImportOptions.All), + symbolValidator: m => + { + VerifyMemorySafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: true, isSynthesized: true); + VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [.. unsafeSymbols], + expectedSafeSymbols: [.. safeSymbols], + expectedAttribute: symbolsWithAttribute); + }) + .VerifyDiagnostics(); + } + + CreateCompilation(source, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (2,2): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] void F() { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(2, 2), + // (5,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] void M() { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(5, 6), + // (6,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] int P { get; set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(6, 6), + // (8,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] event System.Action E { add { } remove { } } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(8, 6), + // (9,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] int this[int i] { get => i; set { } } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(9, 6), + // (10,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] C() { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(10, 6), + // (11,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] public static C operator +(C c1, C c2) => c1; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(11, 6), + // (12,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] public void operator +=(C c) { } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(12, 6)); + } + + [Theory, CombinatorialData] + public void Member_Method_Invocation( + bool apiUpdatedRules, + bool apiUnsafe, + [CombinatorialValues(LanguageVersion.CSharp14, LanguageVersionFacts.CSharpNext, LanguageVersion.Preview)] LanguageVersion callerLangVersion, + bool callerAllowUnsafe, + bool callerUpdatedRules, + bool callerUnsafeBlock, + bool? compilationReference) + { + var requiresUnsafeAttribute = apiUnsafe && apiUpdatedRules + ? "[System.Diagnostics.CodeAnalysis.RequiresUnsafe]" + : ""; + + var api = $$""" + public class C + { + {{requiresUnsafeAttribute}} + public void M() => System.Console.Write(111); + } + """; + + var caller = $""" + var c = new C(); + {(callerUnsafeBlock ? "unsafe { c.M(); }" : "c.M();")} + """; + + var expectedOutput = "111"; + + CSharpCompilation comp; + List expectedDiagnostics = []; + + if (compilationReference is { } useCompilationReference) + { + var apiCompilation = CreateCompilation( + [api, .. (apiUnsafe && apiUpdatedRules ? new[] { RequiresUnsafeAttributeDefinition } : [])], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules(apiUpdatedRules)) + .VerifyDiagnostics(); + var apiReference = AsReference(apiCompilation, useCompilationReference); + comp = CreateCompilation(caller, [apiReference], + parseOptions: TestOptions.Regular.WithLanguageVersion(callerLangVersion), + options: TestOptions.ReleaseExe.WithAllowUnsafe(callerAllowUnsafe).WithUpdatedMemorySafetyRules(callerUpdatedRules)); + } + else + { + if (apiUpdatedRules != callerUpdatedRules) + { + return; + } + + comp = CreateCompilation( + [api, caller, .. (apiUnsafe && apiUpdatedRules ? new[] { RequiresUnsafeAttributeDefinition } : [])], + parseOptions: TestOptions.Regular.WithLanguageVersion(callerLangVersion), + options: TestOptions.ReleaseExe.WithAllowUnsafe(callerAllowUnsafe).WithUpdatedMemorySafetyRules(callerUpdatedRules)); + } + + if (!callerAllowUnsafe && callerUnsafeBlock) + { + expectedDiagnostics.Add( + // (2,1): error CS0227: Unsafe code may only appear if compiling with /unsafe + // unsafe { c.M(); } + Diagnostic(ErrorCode.ERR_IllegalUnsafe, "unsafe").WithLocation(2, 1)); + } + + if (apiUnsafe && apiUpdatedRules && callerUpdatedRules && !callerUnsafeBlock) + { + if (callerLangVersion >= LanguageVersionFacts.CSharpNext) + { + expectedDiagnostics.Add( + // (2,1): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M()").WithArguments("C.M()").WithLocation(2, 1)); + } + else + { + expectedDiagnostics.Add( + // (2,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // c.M(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "c.M()").WithArguments("updated memory safety rules").WithLocation(2, 1)); + } + } + + // When compiling API and caller together (no compilationReference), the [RequiresUnsafe] attribute + // in source also triggers FeatureInPreview if using older language version + if (compilationReference is null && apiUnsafe && apiUpdatedRules && callerLangVersion < LanguageVersionFacts.CSharpNext) + { + expectedDiagnostics.Add( + // (3,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(3, 6)); + } + + if (callerUpdatedRules && callerLangVersion < LanguageVersionFacts.CSharpNext) + { + expectedDiagnostics.Add( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1)); + } + + comp.VerifyDiagnostics([.. expectedDiagnostics]); + + if (!comp.GetDiagnostics().HasAnyErrors()) + { + CompileAndVerify(comp, expectedOutput: expectedOutput).VerifyDiagnostics(); + } + } + + [Fact] + public void Member_Method_OverloadResolution() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public static void M() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static void M(int x) { } + } + """, + caller: """ + C.M(); + C.M(1); + _ = nameof(C.M); + unsafe { C.M(1); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: [Overload("C.M", 1)], + expectedSafeSymbols: ["C", Overload("C.M", 0)], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'C.M(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C.M(1); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.M(1)").WithArguments("C.M(int)").WithLocation(2, 1), + ]); + } + + [Fact] + public void Member_Method_SafeBoundary() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public void M1() { unsafe { M2(); } } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M2() { } + } + """, + caller: """ + var c = new C(); + c.M1(); + c.M2(); + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M2"], + expectedSafeSymbols: ["C.M1"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'C.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M2()").WithArguments("C.M2()").WithLocation(3, 1), + ]); + } + + [Fact] + public void Member_Method_NameOf() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static void M() { } + } + """, + caller: """ + _ = nameof(C.M); + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: []); + } + + [Fact] + public void Member_Method_Extension() + { + CompileAndVerifyUnsafe( + lib: """ + public static class E + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static void M1(this int x) { } + + extension(int x) + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M2() { } + } + } + """, + caller: """ + 123.M1(); + 123.M2(); + E.M1(123); + E.M2(123); + unsafe { 123.M1(); } + unsafe { 123.M2(); } + unsafe { E.M1(123); } + unsafe { E.M2(123); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["E.M1", "E.M2", ExtensionMember("E", "M2")], + expectedSafeSymbols: [], + expectedDiagnostics: + [ + // (1,1): error CS9362: 'E.M1(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // 123.M1(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "123.M1()").WithArguments("E.M1(int)").WithLocation(1, 1), + // (2,1): error CS9362: 'E.extension(int).M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // 123.M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "123.M2()").WithArguments("E.extension(int).M2()").WithLocation(2, 1), + // (3,1): error CS9362: 'E.M1(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E.M1(123); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E.M1(123)").WithArguments("E.M1(int)").WithLocation(3, 1), + // (4,1): error CS9362: 'E.M2(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E.M2(123); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E.M2(123)").WithArguments("E.M2(int)").WithLocation(4, 1), + ]); + } + + [Fact] + public void Member_Method_InUnsafeClass() + { + CompileAndVerifyUnsafe( + lib: """ + using System.Collections.Generic; + public unsafe class C + { + public void M1() { } + public IEnumerable M2() + { + yield return 1; + } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M3() { } + } + """, + caller: """ + var c = new C(); + c.M1(); + c.M2(); + c.M3(); + unsafe { c.M3(); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M3"], + expectedSafeSymbols: ["C.M1", "C.M2"], + expectedDiagnostics: + [ + // (4,1): error CS9362: 'C.M3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M3(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M3()").WithArguments("C.M3()").WithLocation(4, 1), + ]); + } + + [Fact] + public void Member_Method_ConvertToFunctionPointer() + { + CompileAndVerifyUnsafe( + lib: """ + public static class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static void M() { } + } + """, + caller: """ + delegate* p1 = &C.M; + unsafe { delegate* p2 = &C.M; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,22): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // delegate* p1 = &C.M; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "&C.M").WithArguments("C.M()").WithLocation(1, 22), + ], + expectedDiagnosticsForLegacyCaller: + [ + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // delegate* p1 = &C.M; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "delegate*").WithLocation(1, 1), + ]); + } + + [Fact] + public void Member_Method_ConvertToDelegate() + { + CompileAndVerifyUnsafe( + lib: """ + public static class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static void M() { } + } + """, + caller: """ + D1 a = C.M; + D2 b = C.M; + unsafe { D1 c = C.M; } + unsafe { D1 d = C.M; } + + delegate void D1(); + unsafe delegate void D2(); + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,8): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // D1 a = C.M; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.M").WithArguments("C.M()").WithLocation(1, 8), + // (2,8): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // D2 b = C.M; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.M").WithArguments("C.M()").WithLocation(2, 8), + // (7,22): warning CS9377: The 'unsafe' modifier does not have any effect here under the current memory safety rules. + // unsafe delegate void D2(); + Diagnostic(ErrorCode.WRN_UnsafeMeaningless, "D2").WithLocation(7, 22), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (7,22): warning CS9377: The 'unsafe' modifier does not have any effect here under the current memory safety rules. + // unsafe delegate void D2(); + Diagnostic(ErrorCode.WRN_UnsafeMeaningless, "D2").WithLocation(7, 22), + ]); + } + + [Fact] + public void Member_Method_ConvertToDelegate_Inferred() + { + CompileAndVerifyUnsafe( + lib: """ + public static class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static void M() { } + } + """, + caller: """ + var a = C.M; + unsafe { var b = C.M; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,9): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var a = C.M; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.M").WithArguments("C.M()").WithLocation(1, 9), + ]); + + CompileAndVerify( + [ + """ + System.Action a; + unsafe + { + var d = C.M; + System.Console.WriteLine(d.GetType()); + a = d; + } + + a(); + + static class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static void M() => System.Console.WriteLine("ran"); + } + """, + RequiresUnsafeAttributeDefinition, + ], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + expectedOutput: """ + System.Action + ran + """) + .VerifyDiagnostics(); + } + + [Fact] + public void Member_Method_ExpressionTree() + { + CompileAndVerifyUnsafe( + lib: """ + public static class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static void M() { } + } + """, + caller: """ + using System; + using System.Linq.Expressions; + + Expression e1 = () => C.M(); + unsafe { Expression e2 = () => C.M(); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (4,31): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // Expression e1 = () => C.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.M()").WithArguments("C.M()").WithLocation(4, 31), + ]); + } + + [Fact] + public void Member_Method_Attribute() + { + var commonDiagnostics = new[] + { + // (3,4): error CS0617: 'F' is not a valid named attribute argument. Named attribute arguments must be fields which are not readonly, static, or const, or read-write properties which are public and not static. + // [A(F = 0)] void M2() { } + Diagnostic(ErrorCode.ERR_BadNamedAttributeArgument, "F").WithArguments("F").WithLocation(3, 4), + // (4,13): error CS0617: 'F' is not a valid named attribute argument. Named attribute arguments must be fields which are not readonly, static, or const, or read-write properties which are public and not static. + // unsafe { [A(F = 0)] void M3() { } } + Diagnostic(ErrorCode.ERR_BadNamedAttributeArgument, "F").WithArguments("F").WithLocation(4, 13), + // (5,4): error CS0617: 'F' is not a valid named attribute argument. Named attribute arguments must be fields which are not readonly, static, or const, or read-write properties which are public and not static. + // [A(F = 0)] unsafe void M4() { } + Diagnostic(ErrorCode.ERR_BadNamedAttributeArgument, "F").WithArguments("F").WithLocation(5, 4), + }; + + CompileAndVerifyUnsafe( + lib: """ + public class A : System.Attribute + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void F(int x) { } + } + """, + caller: """ + #pragma warning disable CS8321 // unused local function + [A] void M1() { } + [A(F = 0)] void M2() { } + unsafe { [A(F = 0)] void M3() { } } + [A(F = 0)] unsafe void M4() { } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["A.F"], + expectedSafeSymbols: ["A"], + expectedDiagnostics: + [ + // (3,4): error CS9362: 'A.F(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // [A(F = 0)] void M2() { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "F = 0").WithArguments("A.F(int)").WithLocation(3, 4), + .. commonDiagnostics, + ], + expectedDiagnosticsWhenReferencingLegacyLib: commonDiagnostics, + expectedDiagnosticsForLegacyCaller: commonDiagnostics); + } + + [Fact] + public void Member_Method_Interface() + { + CompileAndVerifyUnsafe( + lib: """ + public interface I + { + void M1(); + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void M2(); + } + """, + caller: """ + I i = null; + i.M1(); + i.M2(); + unsafe { i.M2(); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["I.M2"], + expectedSafeSymbols: ["I", "I.M1"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'I.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // i.M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "i.M2()").WithArguments("I.M2()").WithLocation(3, 1), + ]); + } + + [Fact] + public void Member_Method_Override_LangVersion() + { + var lib = """ + public class B + { + public virtual void M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public virtual void M2() { } + } + """; + + var caller = """ + new C().M1(); + + class C : B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M1() { } + public override void M2() { } + } + """; + + var expectedDiagnostics = new[] + { + // (1,1): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // new C().M1(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C().M1()").WithArguments("C.M1()").WithLocation(1, 1), + // (6,26): error CS9364: Unsafe member 'C.M1()' cannot override safe member 'B.M1()' + // public override void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M1").WithArguments("C.M1()", "B.M1()").WithLocation(6, 26), + }; + + CompileAndVerifyUnsafe( + lib: lib, + caller: caller, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["B.M2"], + expectedSafeSymbols: ["B.M1"], + expectedDiagnostics: expectedDiagnostics, + expectedDiagnosticsWhenReferencingLegacyLib: expectedDiagnostics); + + CreateCompilation([lib, caller, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation([lib, caller, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (4,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(4, 6), + // (5,6): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_FeatureInPreview, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("updated memory safety rules").WithLocation(5, 6), + // (6,26): error CS9364: Unsafe member 'C.M1()' cannot override safe member 'B.M1()' + // public override void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M1").WithArguments("C.M1()", "B.M1()").WithLocation(6, 26), + // (1,1): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // new C().M1(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "new C().M1()").WithArguments("updated memory safety rules").WithLocation(1, 1)); + } + + [Fact] + public void Member_Method_Override() + { + CompileAndVerifyUnsafe( + lib: """ + public class B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public virtual void M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public virtual void M2() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public virtual void M3() { } + public virtual void M4() { } + public virtual void M5() { } + public virtual void M6() { } + } + + public class C : B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M1() { } + public new virtual void M2() { } + public override void M3() { } + public override void M4() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public new virtual void M5() { } + } + """, + caller: """ + var d1 = new D1(); d1.M1(); d1.M2(); d1.M3(); d1.M4(); d1.M5(); d1.M6(); + var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + + class D1 : C + { + public override void M1() { } + public override void M2() { } + public override void M3() { } + public override void M4() { } + public override void M5() { } + public override void M6() { } + public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M6(); } + } + + class D2 : C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M2() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M3() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M4() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M5() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M6() { } + } + + class D3 : C; + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["B.M1", "B.M2", "B.M3", "C.M1", "C.M5"], + expectedSafeSymbols: ["B.M4", "B.M5", "B.M6", "C.M2", "C.M3", "C.M4"], + expectedDiagnostics: + [ + // (2,20): error CS9362: 'D2.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M1()").WithArguments("D2.M1()").WithLocation(2, 20), + // (2,29): error CS9362: 'D2.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M2()").WithArguments("D2.M2()").WithLocation(2, 29), + // (2,38): error CS9362: 'D2.M3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M3()").WithArguments("D2.M3()").WithLocation(2, 38), + // (2,47): error CS9362: 'D2.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M4()").WithArguments("D2.M4()").WithLocation(2, 47), + // (2,56): error CS9362: 'D2.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M5()").WithArguments("D2.M5()").WithLocation(2, 56), + // (2,65): error CS9362: 'D2.M6()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M6()").WithArguments("D2.M6()").WithLocation(2, 65), + // (3,20): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M1()").WithArguments("C.M1()").WithLocation(3, 20), + // (3,56): error CS9362: 'C.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M5()").WithArguments("C.M5()").WithLocation(3, 56), + // (4,11): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M1()").WithArguments("C.M1()").WithLocation(4, 11), + // (4,43): error CS9362: 'C.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M5()").WithArguments("C.M5()").WithLocation(4, 43), + // (14,31): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M1()").WithArguments("C.M1()").WithLocation(14, 31), + // (22,26): error CS9364: Unsafe member 'D2.M2()' cannot override safe member 'C.M2()' + // public override void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M2").WithArguments("D2.M2()", "C.M2()").WithLocation(22, 26), + // (26,26): error CS9364: Unsafe member 'D2.M4()' cannot override safe member 'B.M4()' + // public override void M4() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M4").WithArguments("D2.M4()", "B.M4()").WithLocation(26, 26), + // (30,26): error CS9364: Unsafe member 'D2.M6()' cannot override safe member 'B.M6()' + // public override void M6() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M6").WithArguments("D2.M6()", "B.M6()").WithLocation(30, 26), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (2,20): error CS9362: 'D2.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M1()").WithArguments("D2.M1()").WithLocation(2, 20), + // (2,29): error CS9362: 'D2.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M2()").WithArguments("D2.M2()").WithLocation(2, 29), + // (2,38): error CS9362: 'D2.M3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M3()").WithArguments("D2.M3()").WithLocation(2, 38), + // (2,47): error CS9362: 'D2.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M4()").WithArguments("D2.M4()").WithLocation(2, 47), + // (2,56): error CS9362: 'D2.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M5()").WithArguments("D2.M5()").WithLocation(2, 56), + // (2,65): error CS9362: 'D2.M6()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d2.M6()").WithArguments("D2.M6()").WithLocation(2, 65), + // (20,26): error CS9364: Unsafe member 'D2.M1()' cannot override safe member 'B.M1()' + // public override void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M1").WithArguments("D2.M1()", "B.M1()").WithLocation(20, 26), + // (22,26): error CS9364: Unsafe member 'D2.M2()' cannot override safe member 'C.M2()' + // public override void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M2").WithArguments("D2.M2()", "C.M2()").WithLocation(22, 26), + // (24,26): error CS9364: Unsafe member 'D2.M3()' cannot override safe member 'B.M3()' + // public override void M3() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M3").WithArguments("D2.M3()", "B.M3()").WithLocation(24, 26), + // (26,26): error CS9364: Unsafe member 'D2.M4()' cannot override safe member 'B.M4()' + // public override void M4() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M4").WithArguments("D2.M4()", "B.M4()").WithLocation(26, 26), + // (28,26): error CS9364: Unsafe member 'D2.M5()' cannot override safe member 'C.M5()' + // public override void M5() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M5").WithArguments("D2.M5()", "C.M5()").WithLocation(28, 26), + // (30,26): error CS9364: Unsafe member 'D2.M6()' cannot override safe member 'B.M6()' + // public override void M6() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M6").WithArguments("D2.M6()", "B.M6()").WithLocation(30, 26), + ]); + } + + [Fact] + public void Member_Method_Override_Metadata() + { + // [module: MemorySafetyRules(updated)] + // public class A + // { + // public virtual void M() { } + // } + // public class B : A + // { + // [RequiresUnsafe] + // public override void M() { } + // } + var refA = CompileIL($$""" + .assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) } + .assembly '<>' { } + .module '<>.dll' + .custom instance void System.Runtime.CompilerServices.MemorySafetyRulesAttribute::.ctor(int32) = { int32({{CSharpCompilationOptions.UpdatedMemorySafetyRulesVersion}}) } + .class public System.Runtime.CompilerServices.MemorySafetyRulesAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(int32 version) cil managed { ret } + } + .class public System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ret } + } + .class public A + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ret } + .method public hidebysig newslot virtual instance void M() cil managed { ret } + } + .class public B extends A + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ret } + .method public hidebysig virtual instance void M() cil managed + { + .custom instance void System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute::.ctor() + ret + } + } + """, + prependDefaultHeader: false); + + var moduleA = CreateCompilation("", [refA]).GetReferencedAssemblySymbol(refA).Modules.Single(); + VerifyMemorySafetyRulesAttribute( + moduleA, + includesAttributeDefinition: true, + isSynthesized: false, + includesAttributeUse: true); + VerifyRequiresUnsafeAttribute( + moduleA, + expectedUnsafeSymbols: ["B.M"], + expectedSafeSymbols: ["A", "A.M", "B"]); + + CreateCompilation(""" + var c1 = new C1(); + c1.M(); + var c2 = new C2(); + c2.M(); + B b = c2; + b.M(); + A a = b; + a.M(); + + class C1 : B + { + public override void M() { } + } + class C2 : B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M() { } + } + """, + [refA], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (4,1): error CS9362: 'C2.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c2.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c2.M()").WithArguments("C2.M()").WithLocation(4, 1), + // (6,1): error CS9362: 'B.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // b.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "b.M()").WithArguments("B.M()").WithLocation(6, 1), + // (17,26): error CS9364: Unsafe member 'C2.M()' cannot override safe member 'A.M()' + // public override void M() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M").WithArguments("C2.M()", "A.M()").WithLocation(17, 26)); + } + + [Fact] + public void Member_Method_Implementation() + { + CompileAndVerifyUnsafe( + lib: """ + public interface I1 + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void M1(); + void M2(); + } + + public interface I2 + { + void M1(); + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void M2(); + } + + public class C1 : I1 + { + public void M1() { } + public void M2() { } + } + + public class C2 : I1 + { + void I1.M1() { } + void I1.M2() { } + } + """, + caller: """ + var c5 = new C5(); + I1 i1 = c5; + i1.M1(); + i1.M2(); + I2 i2 = c5; + i2.M1(); + i2.M2(); + + public class C3 : I1 + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M2() { } + } + + public class C4 : I1 + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void I1.M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void I1.M2() { } + } + + public class C5 : I1, I2 + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M2() { } + } + + public class C6 : I2, I1 + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M2() { } + } + + public class C7 : I1, I2 + { + public void M1() { } + public void M2() { } + } + + public class C8 : I2, I1 + { + public void M1() { } + public void M2() { } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["I1.M1", "I2.M2"], + expectedSafeSymbols: ["I1.M2", "I2.M1", "C1.M1", "C1.M2", "C2.I1.M1", "C2.I1.M2"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'I1.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // i1.M1(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "i1.M1()").WithArguments("I1.M1()").WithLocation(3, 1), + // (7,1): error CS9362: 'I2.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // i2.M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "i2.M2()").WithArguments("I2.M2()").WithLocation(7, 1), + // (14,17): error CS9365: Unsafe member 'C3.M2()' cannot implicitly implement safe member 'I1.M2()' + // public void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C3.M2()", "I1.M2()").WithLocation(14, 17), + // (22,13): error CS9366: Unsafe member 'C4.I1.M2()' cannot implement safe member 'I1.M2()' + // void I1.M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeExplicitlyImplementingSafe, "M2").WithArguments("C4.I1.M2()", "I1.M2()").WithLocation(22, 13), + // (28,17): error CS9365: Unsafe member 'C5.M1()' cannot implicitly implement safe member 'I2.M1()' + // public void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C5.M1()", "I2.M1()").WithLocation(28, 17), + // (30,17): error CS9365: Unsafe member 'C5.M2()' cannot implicitly implement safe member 'I1.M2()' + // public void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C5.M2()", "I1.M2()").WithLocation(30, 17), + // (36,17): error CS9365: Unsafe member 'C6.M1()' cannot implicitly implement safe member 'I2.M1()' + // public void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C6.M1()", "I2.M1()").WithLocation(36, 17), + // (38,17): error CS9365: Unsafe member 'C6.M2()' cannot implicitly implement safe member 'I1.M2()' + // public void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C6.M2()", "I1.M2()").WithLocation(38, 17), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (12,17): error CS9365: Unsafe member 'C3.M1()' cannot implicitly implement safe member 'I1.M1()' + // public void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C3.M1()", "I1.M1()").WithLocation(12, 17), + // (14,17): error CS9365: Unsafe member 'C3.M2()' cannot implicitly implement safe member 'I1.M2()' + // public void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C3.M2()", "I1.M2()").WithLocation(14, 17), + // (20,13): error CS9366: Unsafe member 'C4.I1.M1()' cannot implement safe member 'I1.M1()' + // void I1.M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeExplicitlyImplementingSafe, "M1").WithArguments("C4.I1.M1()", "I1.M1()").WithLocation(20, 13), + // (22,13): error CS9366: Unsafe member 'C4.I1.M2()' cannot implement safe member 'I1.M2()' + // void I1.M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeExplicitlyImplementingSafe, "M2").WithArguments("C4.I1.M2()", "I1.M2()").WithLocation(22, 13), + // (28,17): error CS9365: Unsafe member 'C5.M1()' cannot implicitly implement safe member 'I1.M1()' + // public void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C5.M1()", "I1.M1()").WithLocation(28, 17), + // (28,17): error CS9365: Unsafe member 'C5.M1()' cannot implicitly implement safe member 'I2.M1()' + // public void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C5.M1()", "I2.M1()").WithLocation(28, 17), + // (30,17): error CS9365: Unsafe member 'C5.M2()' cannot implicitly implement safe member 'I1.M2()' + // public void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C5.M2()", "I1.M2()").WithLocation(30, 17), + // (30,17): error CS9365: Unsafe member 'C5.M2()' cannot implicitly implement safe member 'I2.M2()' + // public void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C5.M2()", "I2.M2()").WithLocation(30, 17), + // (36,17): error CS9365: Unsafe member 'C6.M1()' cannot implicitly implement safe member 'I2.M1()' + // public void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C6.M1()", "I2.M1()").WithLocation(36, 17), + // (36,17): error CS9365: Unsafe member 'C6.M1()' cannot implicitly implement safe member 'I1.M1()' + // public void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C6.M1()", "I1.M1()").WithLocation(36, 17), + // (38,17): error CS9365: Unsafe member 'C6.M2()' cannot implicitly implement safe member 'I2.M2()' + // public void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C6.M2()", "I2.M2()").WithLocation(38, 17), + // (38,17): error CS9365: Unsafe member 'C6.M2()' cannot implicitly implement safe member 'I1.M2()' + // public void M2() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C6.M2()", "I1.M2()").WithLocation(38, 17), + ]); + } + + [Fact] + public void Member_Method_Implementation_Synthesized() + { + CompileAndVerifyUnsafe( + lib: """ + public class B + { + public void M1(T x) { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M2(T x) { } + } + """, + caller: """ + var c = new C(); + c.M1(null); + c.M2(null); + + class C : B, I; + + class D; + + interface I + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void M1(D x); + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["B.M2"], + expectedSafeSymbols: ["B.M1"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'B.M2(D)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M2(null)").WithArguments("B.M2(D)").WithLocation(3, 1), + ]); + } + + [Fact] + public void Member_Method_OverrideOfImplementation() + { + CompileAndVerifyUnsafe( + lib: """ + public interface I + { + void M1(); + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void M2(); + } + + public class B1 : I + { + public virtual void M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public virtual void M2() { } + } + """, + caller: """ + var c1 = new C1(); + c1.M1(); + c1.M2(); + B1 b1 = c1; + b1.M1(); + b1.M2(); + I i = b1; + i.M1(); + i.M2(); + + class B2 : I + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public virtual void M1() { } + public virtual void M2() { } + } + + class C1 : B1 + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M1() { } + public override void M2() { } + } + + class C2 : B1, I + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override void M1() { } + public override void M2() { } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["I.M2", "B1.M2"], + expectedSafeSymbols: ["I.M1", "B1.M1"], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'C1.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c1.M1(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c1.M1()").WithArguments("C1.M1()").WithLocation(2, 1), + // (6,1): error CS9362: 'B1.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // b1.M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "b1.M2()").WithArguments("B1.M2()").WithLocation(6, 1), + // (9,1): error CS9362: 'I.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // i.M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "i.M2()").WithArguments("I.M2()").WithLocation(9, 1), + // (14,25): error CS9365: Unsafe member 'B2.M1()' cannot implicitly implement safe member 'I.M1()' + // public virtual void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("B2.M1()", "I.M1()").WithLocation(14, 25), + // (21,26): error CS9364: Unsafe member 'C1.M1()' cannot override safe member 'B1.M1()' + // public override void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M1").WithArguments("C1.M1()", "B1.M1()").WithLocation(21, 26), + // (28,26): error CS9364: Unsafe member 'C2.M1()' cannot override safe member 'B1.M1()' + // public override void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M1").WithArguments("C2.M1()", "B1.M1()").WithLocation(28, 26), + // (28,26): error CS9365: Unsafe member 'C2.M1()' cannot implicitly implement safe member 'I.M1()' + // public override void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C2.M1()", "I.M1()").WithLocation(28, 26), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (2,1): error CS9362: 'C1.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c1.M1(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c1.M1()").WithArguments("C1.M1()").WithLocation(2, 1), + // (14,25): error CS9365: Unsafe member 'B2.M1()' cannot implicitly implement safe member 'I.M1()' + // public virtual void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("B2.M1()", "I.M1()").WithLocation(14, 25), + // (21,26): error CS9364: Unsafe member 'C1.M1()' cannot override safe member 'B1.M1()' + // public override void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M1").WithArguments("C1.M1()", "B1.M1()").WithLocation(21, 26), + // (28,26): error CS9364: Unsafe member 'C2.M1()' cannot override safe member 'B1.M1()' + // public override void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M1").WithArguments("C2.M1()", "B1.M1()").WithLocation(28, 26), + // (28,26): error CS9365: Unsafe member 'C2.M1()' cannot implicitly implement safe member 'I.M1()' + // public override void M1() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C2.M1()", "I.M1()").WithLocation(28, 26), + ]); + } + + /// + /// Caller-unsafety should not count as part of the signature for hiding purposes. + /// + [Fact] + public void Member_Method_Hiding() + { + DiagnosticDescription[] commonDiagnostics = + [ + // (7,17): warning CS0108: 'C1.M1()' hides inherited member 'B.M1()'. Use the new keyword if hiding was intended. + // public void M1() { } + Diagnostic(ErrorCode.WRN_NewRequired, "M1").WithArguments("C1.M1()", "B.M1()").WithLocation(7, 17), + // (9,17): warning CS0108: 'C1.M2()' hides inherited member 'B.M2()'. Use the new keyword if hiding was intended. + // public void M2() { } + Diagnostic(ErrorCode.WRN_NewRequired, "M2").WithArguments("C1.M2()", "B.M2()").WithLocation(9, 17), + // (16,17): error CS0111: Type 'C2' already defines a member called 'M1' with the same parameter types + // public void M1() { } + Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "M1").WithArguments("M1", "C2").WithLocation(16, 17), + ]; + + DiagnosticDescription[] updatedCallerDiagnostics = + [ + // (3,1): error CS9362: 'C1.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M2()").WithArguments("C1.M2()").WithLocation(3, 1), + .. commonDiagnostics, + ]; + + CompileAndVerifyUnsafe( + lib: """ + public class B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M1() { } + public void M2() { } + } + """, + caller: """ + var c = new C1(); + c.M1(); + c.M2(); + + class C1 : B + { + public void M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M2() { } + } + + class C2 + { + public void M1() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M1() { } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["B.M1"], + expectedSafeSymbols: ["B.M2"], + expectedDiagnostics: updatedCallerDiagnostics, + expectedDiagnosticsWhenReferencingLegacyLib: updatedCallerDiagnostics, + expectedDiagnosticsForLegacyCaller: commonDiagnostics); + } + + [Fact] + public void Member_Await() + { + static string getLib(string onCompletedAttribute) => $$""" + using System; + using System.Runtime.CompilerServices; + + public class C : INotifyCompletion + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public bool IsCompleted => false; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C GetAwaiter() => this; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void GetResult() { } + {{onCompletedAttribute}} + public void OnCompleted(Action continuation) { } + } + """; + + CreateCompilation( + [getLib("[System.Diagnostics.CodeAnalysis.RequiresUnsafe]"), RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (13,17): error CS9365: Unsafe member 'C.OnCompleted(Action)' cannot implicitly implement safe member 'INotifyCompletion.OnCompleted(Action)' + // public void OnCompleted(Action continuation) { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "OnCompleted").WithArguments("C.OnCompleted(System.Action)", "System.Runtime.CompilerServices.INotifyCompletion.OnCompleted(System.Action)").WithLocation(13, 17)); + + var lib = getLib(""); + + CompileAndVerifyUnsafe( + lib: lib, + caller: """ + await new C(); + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.IsCompleted", "C.GetAwaiter", "C.GetResult"], + expectedSafeSymbols: ["C", "C.OnCompleted"], + expectedDiagnostics: + [ + // (1,1): error CS9362: 'C.GetAwaiter()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // await new C(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "await new C()").WithArguments("C.GetAwaiter()").WithLocation(1, 1), + // (1,1): error CS9362: 'C.IsCompleted.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // await new C(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "await new C()").WithArguments("C.IsCompleted.get").WithLocation(1, 1), + // (1,1): error CS9362: 'C.GetResult()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // await new C(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "await new C()").WithArguments("C.GetResult()").WithLocation(1, 1), + ]); + + CreateCompilation( + [ + lib, + RequiresUnsafeAttributeDefinition, + """ + unsafe { await new C(); } + """, + ], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,10): error CS4004: Cannot await in an unsafe context + // unsafe { await new C(); } + Diagnostic(ErrorCode.ERR_AwaitInUnsafeContext, "await new C()").WithLocation(1, 10)); + } + + [Fact] + public void Member_AsyncHelpers() + { + var corlib = CreateEmptyCompilation( + """ + namespace System + { + public class Object; + public class ValueType; + public class Attribute; + public struct Void; + public struct Int32; + public struct Boolean; + public class AttributeUsageAttribute + { + public AttributeUsageAttribute(AttributeTargets t) { } + public bool AllowMultiple { get; set; } + public bool Inherited { get; set; } + } + public class Enum; + public enum AttributeTargets; + public class String; + public class Action; + } + namespace System.Diagnostics.CodeAnalysis + { + public class RequiresUnsafeAttribute : Attribute { } + } + namespace System.Runtime.CompilerServices + { + public interface INotifyCompletion; + public static class AsyncHelpers + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static void AwaitAwaiter(TAwaiter awaiter) where TAwaiter : INotifyCompletion { } + } + } + namespace System.Threading.Tasks + { + public class Task : System.Runtime.CompilerServices.INotifyCompletion + { + public bool IsCompleted => false; + public Task GetAwaiter() => this; + public void GetResult() { } + public void OnCompleted(Action continuation) { } + } + } + """, + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics() + .EmitToImageReference(); + + CreateEmptyCompilation(""" + using System.Threading.Tasks; + + class Test + { + public static async Task F() + { + await new Task(); + } + } + """, + [corlib], + parseOptions: WithRuntimeAsync(TestOptions.RegularPreview), + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (7,15): error CS9362: 'AsyncHelpers.AwaitAwaiter(Task)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // await new Task(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new Task()").WithArguments("System.Runtime.CompilerServices.AsyncHelpers.AwaitAwaiter(System.Threading.Tasks.Task)").WithLocation(7, 15)); + + CreateEmptyCompilation(""" + using System.Threading.Tasks; + + class Test + { + public static async Task F() + { + unsafe { await new Task(); } + } + } + """, + [corlib], + parseOptions: WithRuntimeAsync(TestOptions.RegularPreview), + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (7,18): error CS4004: Cannot await in an unsafe context + // unsafe { await new Task(); } + Diagnostic(ErrorCode.ERR_AwaitInUnsafeContext, "await new Task()").WithLocation(7, 18)); + } + + [Fact] + public void Member_CollectionBuilder_Create() + { + CompileAndVerifyUnsafe( + lib: """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Runtime.CompilerServices; + + [CollectionBuilder(typeof(C), nameof(Create))] + public class C : IEnumerable + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static C Create(ReadOnlySpan s) => new C(); + public IEnumerator GetEnumerator() => throw null; + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + """, + caller: """ + C c1 = [1, 2, 3]; + M(1, 2, 3); + static void M(params C c) { } + + unsafe + { + C c2 = [1, 2, 3]; + M2(1, 2, 3); + static void M2(params C c) { } + } + + M3(1, 2, 3); + static unsafe void M3(params C c) { } + """, + additionalSources: [TestSources.Span, CollectionBuilderAttributeDefinition, RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["C.Create"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,8): error CS9362: 'C.Create(ReadOnlySpan)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c1 = [1, 2, 3]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "[1, 2, 3]").WithArguments("C.Create(System.ReadOnlySpan)").WithLocation(1, 8), + // (2,1): error CS9362: 'C.Create(ReadOnlySpan)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "M(1, 2, 3)").WithArguments("C.Create(System.ReadOnlySpan)").WithLocation(2, 1), + // (3,15): error CS9362: 'C.Create(ReadOnlySpan)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // static void M(params C c) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "params C c").WithArguments("C.Create(System.ReadOnlySpan)").WithLocation(3, 15), + // (12,1): error CS9362: 'C.Create(ReadOnlySpan)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M3(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "M3(1, 2, 3)").WithArguments("C.Create(System.ReadOnlySpan)").WithLocation(12, 1), + ]); + } + + [Fact] + public void Member_CollectionBuilder_GetEnumerator() + { + CompileAndVerifyUnsafe( + lib: """ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Runtime.CompilerServices; + + [CollectionBuilder(typeof(C), nameof(Create))] + public class C : IEnumerable + { + public static C Create(ReadOnlySpan s) => new C(); + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public IEnumerator GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + } + """, + caller: """ + C c1 = [1, 2, 3]; + M(1, 2, 3); + static void M(params C c) { } + """, + additionalSources: [TestSources.Span, CollectionBuilderAttributeDefinition, RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["C.GetEnumerator"], + expectedSafeSymbols: ["C", "C.Create"], + expectedDiagnostics: []); + } + + [Fact] + public void Member_CollectionConstructor() + { + CompileAndVerifyUnsafe( + lib: """ + using System.Collections; + using System.Collections.Generic; + + public class C : IEnumerable + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + public void Add(int x) { } + public IEnumerator GetEnumerator() => throw null; + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + """, + caller: """ + C c1 = [1, 2, 3]; + M1(1, 2, 3); + static void M1(params C c) { } + + unsafe + { + C c2 = [1, 2, 3]; + M2(1, 2, 3); + static void M2(params C c) { } + } + + M3(1, 2, 3); + static unsafe void M3(params C c) { } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,8): error CS9362: 'C.C()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c1 = [1, 2, 3]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "[1, 2, 3]").WithArguments("C.C()").WithLocation(1, 8), + // (2,1): error CS9362: 'C.C()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M1(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "M1(1, 2, 3)").WithArguments("C.C()").WithLocation(2, 1), + // (3,16): error CS9362: 'C.C()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // static void M1(params C c) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "params C c").WithArguments("C.C()").WithLocation(3, 16), + // (12,1): error CS9362: 'C.C()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M3(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "M3(1, 2, 3)").WithArguments("C.C()").WithLocation(12, 1), + ]); + } + + [Fact] + public void Member_CollectionConstructor_With() + { + CompileAndVerifyUnsafe( + lib: """ + using System.Collections; + using System.Collections.Generic; + + public class C : IEnumerable + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C(int i) { } + public void Add(int x) { } + public IEnumerator GetEnumerator() => throw null; + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + """, + caller: """ + C c1 = [with(0), 1, 2, 3]; + unsafe { C c2 = [with(0), 1, 2, 3]; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,9): error CS9362: 'C.C(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c1 = [with(0), 1, 2, 3]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "with(0)").WithArguments("C.C(int)").WithLocation(1, 9), + ]); + } + + [Fact] + public void Member_CollectionAddMethod() + { + CompileAndVerifyUnsafe( + lib: """ + using System.Collections; + using System.Collections.Generic; + + public class C : IEnumerable + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void Add(int x) { } + public IEnumerator GetEnumerator() => throw null; + IEnumerator IEnumerable.GetEnumerator() => throw null; + } + """, + caller: """ + C c1 = [1, 2, 3]; + C c2 = new() { 1, 2, 3 }; + C c3 = [.. new C()]; + X x1 = new() { F = { 1, 2, 3 } }; + M1(1, 2, 3); + static void M1(params C c) { } + + unsafe + { + C c4 = [1, 2, 3]; + C c5 = new() { 1, 2, 3 }; + C c6 = [.. new C()]; + X x2 = new() { F = { 1, 2, 3 } }; + M2(1, 2, 3); + static void M2(params C c) { } + } + + M3(1, 2, 3); + static unsafe void M3(params C c) { } + + class X { public C F; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.Add"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,9): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c1 = [1, 2, 3]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "1").WithArguments("C.Add(int)").WithLocation(1, 9), + // (1,12): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c1 = [1, 2, 3]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "2").WithArguments("C.Add(int)").WithLocation(1, 12), + // (1,15): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c1 = [1, 2, 3]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "3").WithArguments("C.Add(int)").WithLocation(1, 15), + // (2,16): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c2 = new() { 1, 2, 3 }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "1").WithArguments("C.Add(int)").WithLocation(2, 16), + // (2,19): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c2 = new() { 1, 2, 3 }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "2").WithArguments("C.Add(int)").WithLocation(2, 19), + // (2,22): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c2 = new() { 1, 2, 3 }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "3").WithArguments("C.Add(int)").WithLocation(2, 22), + // (3,12): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c3 = [.. new C()]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C()").WithArguments("C.Add(int)").WithLocation(3, 12), + // (4,22): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // X x1 = new() { F = { 1, 2, 3 } }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "1").WithArguments("C.Add(int)").WithLocation(4, 22), + // (4,25): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // X x1 = new() { F = { 1, 2, 3 } }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "2").WithArguments("C.Add(int)").WithLocation(4, 25), + // (4,28): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // X x1 = new() { F = { 1, 2, 3 } }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "3").WithArguments("C.Add(int)").WithLocation(4, 28), + // (5,4): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M1(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "1").WithArguments("C.Add(int)").WithLocation(5, 4), + // (5,7): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M1(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "2").WithArguments("C.Add(int)").WithLocation(5, 7), + // (5,10): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M1(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "3").WithArguments("C.Add(int)").WithLocation(5, 10), + // (6,16): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // static void M1(params C c) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "params C c").WithArguments("C.Add(int)").WithLocation(6, 16), + // (18,4): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M3(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "1").WithArguments("C.Add(int)").WithLocation(18, 4), + // (18,7): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M3(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "2").WithArguments("C.Add(int)").WithLocation(18, 7), + // (18,10): error CS9362: 'C.Add(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M3(1, 2, 3); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "3").WithArguments("C.Add(int)").WithLocation(18, 10), + ]); + } + + [Fact] + public void Member_GetEnumerator() + { + CompileAndVerifyUnsafe( + lib: """ + using System.Collections; + using System.Collections.Generic; + + public class C : IEnumerable + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public IEnumerator GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + } + """, + caller: """ + foreach (var c in new C()) { } + unsafe { foreach (var c in new C()) { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.GetEnumerator"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,1): error CS9362: 'C.GetEnumerator()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // foreach (var c in new C()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "foreach").WithArguments("C.GetEnumerator()").WithLocation(1, 1), + ]); + } + + [Fact] + public void Member_GetEnumerator_Spread() + { + CompileAndVerifyUnsafe( + lib: """ + using System.Collections; + using System.Collections.Generic; + + public class C : IEnumerable + { + public void Add(int x) { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C GetEnumerator() => this; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public bool MoveNext() => false; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int Current => 0; + IEnumerator IEnumerable.GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; + } + """, + caller: """ + C c1 = [.. new C()]; + unsafe { C c2 = [.. new C()]; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.GetEnumerator", "C.MoveNext", "C.Current", "C.get_Current"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,9): error CS9362: 'C.GetEnumerator()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c1 = [.. new C()]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "..").WithArguments("C.GetEnumerator()").WithLocation(1, 9), + // (1,9): error CS9362: 'C.MoveNext()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c1 = [.. new C()]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "..").WithArguments("C.MoveNext()").WithLocation(1, 9), + // (1,9): error CS9362: 'C.Current.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c1 = [.. new C()]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "..").WithArguments("C.Current.get").WithLocation(1, 9), + ]); + } + + [Fact] + public void Member_MoveNext() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public C GetEnumerator() => this; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public bool MoveNext() => false; + public int Current => 0; + } + """, + caller: """ + foreach (var x in new C()) { } + unsafe { foreach (var x in new C()) { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.MoveNext"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,1): error CS9362: 'C.MoveNext()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // foreach (var x in new C()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "foreach").WithArguments("C.MoveNext()").WithLocation(1, 1), + ]); + } + + [Theory] + [InlineData("=> 0;")] + [InlineData("{ get => 0; }")] + public void Member_Current_Property(string getter) + { + CompileAndVerifyUnsafe( + lib: $$""" + public class C + { + public C GetEnumerator() => this; + public bool MoveNext() => false; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int Current {{getter}} + } + """, + caller: """ + foreach (var x in new C()) { } + unsafe { foreach (var x in new C()) { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.Current", "C.get_Current"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,1): error CS9362: 'C.Current.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // foreach (var x in new C()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "foreach").WithArguments("C.Current.get").WithLocation(1, 1), + ]); + } + + [Fact] + public void Member_Current_Getter() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public C GetEnumerator() => this; + public bool MoveNext() => false; + public int Current { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get => 0; } + } + """, + caller: """ + foreach (var x in new C()) { } + unsafe { foreach (var x in new C()) { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.get_Current"], + expectedSafeSymbols: ["C", "C.Current"], + expectedDiagnostics: + [ + // (1,1): error CS9362: 'C.Current.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // foreach (var x in new C()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "foreach").WithArguments("C.Current.get").WithLocation(1, 1), + ]); + } + + [Fact] + public void Member_Current_Setter() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public C GetEnumerator() => this; + public bool MoveNext() => false; + public int Current { get => 0; [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set { } } + } + """, + caller: """ + foreach (var x in new C()) { } + unsafe { foreach (var x in new C()) { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.set_Current"], + expectedSafeSymbols: ["C", "C.Current", "C.get_Current"], + expectedDiagnostics: []); + } + + [Fact] + public void Member_Dispose_Class() + { + CreateCompilation([""" + public class C : System.IDisposable + { + public C GetEnumerator() => this; + public bool MoveNext() => false; + public int Current => 0; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void Dispose() { } + } + """, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (7,17): error CS9365: Unsafe member 'C.Dispose()' cannot implicitly implement safe member 'IDisposable.Dispose()' + // public unsafe void Dispose() { } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "Dispose").WithArguments("C.Dispose()", "System.IDisposable.Dispose()").WithLocation(7, 17)); + } + + [Fact] + public void Member_Dispose_Interface() + { + CompileAndVerifyUnsafe( + lib: """ + public class C : System.IDisposable + { + public C GetEnumerator() => this; + public bool MoveNext() => false; + public int Current => 0; + public void Dispose() { } + } + + namespace System + { + public class Object; + public class ValueType; + public class Attribute; + public struct Void; + public struct Int32; + public struct Boolean; + public class AttributeUsageAttribute + { + public AttributeUsageAttribute(AttributeTargets t) { } + public bool AllowMultiple { get; set; } + public bool Inherited { get; set; } + } + public class String; + public class Enum; + public enum AttributeTargets; + public interface IDisposable + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void Dispose(); + } + } + + namespace System.Diagnostics.CodeAnalysis + { + public class RequiresUnsafeAttribute : Attribute; + } + + namespace System.Runtime.CompilerServices + { + public class CompilerGeneratedAttribute; + } + + namespace System.Collections + { + public interface IEnumerable; + } + + namespace System.Collections.Generic + { + public interface IEnumerable : IEnumerable; + public class List : IEnumerable + { + public void Add(T element) { } + } + } + """, + caller: """ + foreach (var x in new C()) { } + using (var c = new C()) { } + System.Collections.Generic.List l = [.. new C()]; + unsafe { foreach (var x in new C()) { } } + unsafe { using (var c = new C()) { } } + unsafe { System.Collections.Generic.List l2 = [.. new C()]; } + """, + targetFramework: TargetFramework.Empty, + optionsDll: TestOptions.UnsafeDebugDll + // warning CS8021: No value for RuntimeMetadataVersion found + .WithSpecificDiagnosticOptions(GetIdForErrorCode(ErrorCode.WRN_NoRuntimeMetadataVersion), ReportDiagnostic.Suppress), + expectedUnsafeSymbols: ["System.IDisposable.Dispose"], + expectedSafeSymbols: ["C", "C.Dispose", "System.IDisposable"], + verify: Verification.FailsPEVerify, + expectedDiagnostics: + [ + // (1,1): error CS9362: 'IDisposable.Dispose()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // foreach (var x in new C()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "foreach").WithArguments("System.IDisposable.Dispose()").WithLocation(1, 1), + // (2,8): error CS9362: 'IDisposable.Dispose()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // using (var c = new C()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "var c = new C()").WithArguments("System.IDisposable.Dispose()").WithLocation(2, 8), + // (3,43): error CS9362: 'IDisposable.Dispose()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // System.Collections.Generic.List l = [.. new C()]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "..").WithArguments("System.IDisposable.Dispose()").WithLocation(3, 43), + ]); + } + + [Fact] + public void Member_Dispose_RefStruct() + { + CompileAndVerifyUnsafe( + lib: """ + public ref struct S + { + public S GetEnumerator() => this; + public bool MoveNext() => false; + public int Current => 0; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void Dispose() { } + } + """, + caller: """ + foreach (var x in new S()) { } + using (var s = new S()) { } + unsafe { foreach (var y in new S()) { } } + unsafe { using (var s = new S()) { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["S.Dispose"], + expectedSafeSymbols: ["S"], + expectedDiagnostics: + [ + // (1,1): error CS9362: 'S.Dispose()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // foreach (var x in new S()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "foreach (var x in new S()) { }").WithArguments("S.Dispose()").WithLocation(1, 1), + // (2,8): error CS9362: 'S.Dispose()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // using (var s = new S()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "var s = new S()").WithArguments("S.Dispose()").WithLocation(2, 8), + ]); + } + + [Fact] + public void Member_DisposeAsync() + { + CompileAndVerifyUnsafe( + lib: """ + using System.Threading.Tasks; + public class C + { + public C GetAsyncEnumerator() => this; + public Task MoveNextAsync() => null; + public int Current => 0; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public Task DisposeAsync() => null; + } + """, + caller: """ + await foreach (var x in new C()) { } + await using (var c = new C()) { } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.DisposeAsync"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,1): error CS9362: 'C.DisposeAsync()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // await foreach (var x in new C()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "await foreach (var x in new C()) { }").WithArguments("C.DisposeAsync()").WithLocation(1, 1), + // (2,14): error CS9362: 'C.DisposeAsync()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // await using (var c = new C()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "var c = new C()").WithArguments("C.DisposeAsync()").WithLocation(2, 14), + ]); + } + + [Fact] + public void Member_GetPinnableReference() + { + CompileAndVerifyUnsafe( + lib: """ + public class C1 { public ref int GetPinnableReference() => throw null; } + public class C2 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] public ref int GetPinnableReference() => throw null; } + """, + caller: """ + fixed (int* p = new C1()) { } + fixed (int* p = new C2()) { } + unsafe { fixed (int* p = new C2()) { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C2.GetPinnableReference"], + expectedSafeSymbols: ["C1", "C1.GetPinnableReference", "C2"], + expectedDiagnostics: + [ + // (2,17): error CS9362: 'C2.GetPinnableReference()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // fixed (int* p = new C2()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C2()").WithArguments("C2.GetPinnableReference()").WithLocation(2, 17), + ], + expectedDiagnosticsForLegacyCaller: + [ + // (1,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = new C1()) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "fixed (int* p = new C1()) { }").WithLocation(1, 1), + // (1,8): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = new C1()) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 8), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = new C2()) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "fixed (int* p = new C2()) { }").WithLocation(2, 1), + // (2,8): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // fixed (int* p = new C2()) { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(2, 8), + ]); + } + + [Fact] + public void Member_Deconstruct() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void Deconstruct(out int x, out int y) { x = y = 0; } + } + """, + caller: """ + var (x, y) = new C(); + unsafe { var (a, b) = new C(); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.Deconstruct"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,14): error CS9362: 'C.Deconstruct(out int, out int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var (x, y) = new C(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C()").WithArguments("C.Deconstruct(out int, out int)").WithLocation(1, 14), + ]); + } + + [Fact] + public void Member_LockObject() + { + CompileAndVerifyUnsafe( + lib: """ + namespace System.Threading; + public class Lock + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public Scope EnterScope() => new(); + public ref struct Scope + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void Dispose() { } + } + } + """, + caller: """ + lock (new System.Threading.Lock()) { } + unsafe { lock (new System.Threading.Lock()) { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["System.Threading.Lock.EnterScope", "System.Threading.Lock.Scope.Dispose"], + expectedSafeSymbols: ["System.Threading.Lock", "System.Threading.Lock.Scope"], + expectedDiagnostics: + [ + // (1,7): error CS9362: 'Lock.EnterScope()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // lock (new System.Threading.Lock()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new System.Threading.Lock()").WithArguments("System.Threading.Lock.EnterScope()").WithLocation(1, 7), + // (1,7): error CS9362: 'Lock.Scope.Dispose()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // lock (new System.Threading.Lock()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new System.Threading.Lock()").WithArguments("System.Threading.Lock.Scope.Dispose()").WithLocation(1, 7), + ]); + } + + [Fact] + public void Member_ITuple() + { + CompileAndVerifyUnsafe( + lib: """ + namespace System.Runtime.CompilerServices; + public interface ITuple + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + int Length { get; } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + object this[int index] { get; } + } + """, + caller: """ + object o = null; + _ = o is (int x, string y); + unsafe { _ = o is (int a, string b); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["System.Runtime.CompilerServices.ITuple.Length", "System.Runtime.CompilerServices.ITuple.get_Length", "System.Runtime.CompilerServices.ITuple.this[]", "System.Runtime.CompilerServices.ITuple.get_Item"], + expectedSafeSymbols: ["System.Runtime.CompilerServices.ITuple"], + expectedDiagnostics: + [ + // (2,10): error CS9362: 'ITuple.Length.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = o is (int x, string y); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "(int x, string y)").WithArguments("System.Runtime.CompilerServices.ITuple.Length.get").WithLocation(2, 10), + // (2,10): error CS9362: 'ITuple.this[int].get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = o is (int x, string y); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "(int x, string y)").WithArguments("System.Runtime.CompilerServices.ITuple.this[int].get").WithLocation(2, 10), + ]); + } + + [Fact] + public void Member_InterpolatedStringHandler() + { + CompileAndVerifyUnsafe( + lib: """ + [System.Runtime.CompilerServices.InterpolatedStringHandler] + public struct C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C(int literalLength, int formattedCount) { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void AppendLiteral(string s) { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void AppendFormatted(T t) { } + } + """, + caller: """ + log($"a{0}"); + unsafe { log($"a{0}"); }; + void log(C c) { } + """, + additionalSources: [InterpolatedStringHandlerAttribute, RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: [Overload("C..ctor", parameterCount: 2), "C.AppendLiteral", "C.AppendFormatted"], + expectedSafeSymbols: ["C", Overload("C..ctor", parameterCount: 0)], + expectedDiagnostics: + [ + // (1,7): error CS9362: 'C.AppendLiteral(string)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // log($"a{0}"); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "a").WithArguments("C.AppendLiteral(string)").WithLocation(1, 7), + // (1,8): error CS9362: 'C.AppendFormatted(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // log($"a{0}"); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "{0}").WithArguments("C.AppendFormatted(int)").WithLocation(1, 8), + // (1,5): error CS9362: 'C.C(int, int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // log($"a{0}"); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, @"$""a{0}""").WithArguments("C.C(int, int)").WithLocation(1, 5), + ]); + } + + [Theory, CombinatorialData] + public void Member_Interceptor( + [CombinatorialValues("[System.Diagnostics.CodeAnalysis.RequiresUnsafe]", "")] string unsafe1, + [CombinatorialValues("[System.Diagnostics.CodeAnalysis.RequiresUnsafe]", "")] string unsafe2) + { + var source = ($$""" + C.M(); + + class C + { + {{unsafe1}} + public static void M() { } + } + """, "Program.cs"); + + var comp = CreateCompilation(source); + var tree = comp.SyntaxTrees.Single(); + var model = comp.GetSemanticModel(tree); + var interceptableLocation = tree.GetRoot().DescendantNodes().OfType().Select(node => model.GetInterceptableLocation(node)).Single()!; + + var interceptor = ($$""" + class D + { + {{interceptableLocation.GetInterceptsLocationAttributeSyntax()}} + {{unsafe2}} + public static void M() => System.Console.Write(1); + } + """, "Interceptor.cs"); + + CreateCompilation([source, interceptor, (TestSources.InterceptsLocationAttribute, "Attribute.cs"), RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.RegularPreview.WithFeature(Feature.InterceptorsNamespaces, "global"), + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(unsafe1.Length > 0 + ? [ + // Program.cs(1,1): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.M()").WithArguments("C.M()").WithLocation(1, 1), + ] + : []); + } + + [Fact] + public void Member_Iterator() + { + var lib = """ + using System.Collections.Generic; + + public static class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static IEnumerable M() + { + yield return 1; + } + } + """; + + CompileAndVerifyUnsafe( + lib: lib, + caller: """ + foreach (var x in C.M()) { } + unsafe { foreach (var y in C.M()) { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,19): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // foreach (var x in C.M()) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.M()").WithArguments("C.M()").WithLocation(1, 19), + ]); + + // Test symbols that are only in PE image (hence cannot test them via the helper above). + var libRef = CreateCompilation( + [lib, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules().WithMetadataImportOptions(MetadataImportOptions.All)) + .EmitToImageReference(); + var libAssemblySymbol = CreateCompilation("", [libRef]).GetReferencedAssemblySymbol(libRef); + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C", "C.d__0", "C.d__0.MoveNext"]); + } + + [Fact] + public void Member_LocalFunction() + { + var source = """ + M1(); + M2(); + unsafe { M1(); } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + static void M1() { } + static void M2() { } + """; + CreateCompilation([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,1): error CS9362: 'M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // M1(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "M1()").WithArguments("M1()").WithLocation(1, 1)); + } + + [Fact] + public void Member_Lambda() + { + var source = """ + var lam = [System.Diagnostics.CodeAnalysis.RequiresUnsafe] () => { }; + lam(); + """; + CreateCompilation([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,12): error CS9367: RequiresUnsafeAttribute cannot be applied to this symbol. + // var lam = [System.Diagnostics.CodeAnalysis.RequiresUnsafe] () => { }; + Diagnostic(ErrorCode.ERR_RequiresUnsafeAttributeUnsupportedMemberTarget, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(1, 12)); + } + + [Fact] + public void Member_Lambda_InUnsafeContext() + { + var source = """ + unsafe + { + D lam = () => { }; + } + + delegate void D(); + """; + var metadata = CreateCompilation(source, + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics() + .EmitToImageReference(); + var assemblySymbol = CreateCompilation("", [metadata], + options: TestOptions.ReleaseDll.WithMetadataImportOptions(MetadataImportOptions.All)) + .GetReferencedAssemblySymbol(metadata); + VerifyRequiresUnsafeAttribute( + assemblySymbol.Modules.Single(), + expectedUnsafeSymbols: [], + expectedSafeSymbols: + [ + "Program", + "Program.
$", + "Program.<>c", + "Program.<>c.<
$>b__0_0", + "D", + "D..ctor", + "D.Invoke", + "D.BeginInvoke", + "D.EndInvoke", + ]); + } + + [Fact] + public void Member_Property() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public int P1 { get; set; } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int P2 { get; set; } + } + """, + caller: """ + var c = new C(); + c.P1 = c.P1 + 123; + c.P2 = c.P2 + 123; + unsafe { c.P2 = c.P2 + 123; } + """, + optionsDll: TestOptions.UnsafeReleaseDll.WithMetadataImportOptions(MetadataImportOptions.All), + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.P2", "C.get_P2", "C.set_P2"], + expectedSafeSymbols: ["C.P1", "C.get_P1", "C.set_P1", "C.k__BackingField", "C.k__BackingField"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'C.P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = c.P2 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C.P2.set").WithLocation(3, 1), + // (3,8): error CS9362: 'C.P2.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = c.P2 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C.P2.get").WithLocation(3, 8) + ]); + } + + [Fact] + public void Member_Property_CompoundAssignment() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int P { get; set; } + } + """, + caller: """ + var c = new C(); + c.P += 123; + unsafe { c.P += 123; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.P", "C.get_P", "C.set_P"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'C.P.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P += 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P").WithArguments("C.P.set").WithLocation(2, 1), + // (2,1): error CS9362: 'C.P.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P += 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P").WithArguments("C.P.get").WithLocation(2, 1), + ]); + } + + [Fact] + public void Member_Property_ObjectInitializer() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C P1 { get; set; } + public C P2 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + public C P3 { get; [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set; } + public C P4 { get; set; } + } + """, + caller: """ + _ = new C + { + P1 = null, + P2 = null, + P3 = null, + P4 = null, + }; + _ = new C + { + P1 = { }, + P2 = { }, + P3 = { }, + P4 = { }, + }; + unsafe { _ = new C { P1 = null, P2 = null, P3 = null, P4 = null }; } + unsafe { _ = new C { P1 = { }, P2 = { }, P3 = { }, P4 = { } }; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.P1", "C.get_P1", "C.set_P1", "C.get_P2", "C.set_P3"], + expectedSafeSymbols: ["C", "C.P2", "C.set_P2", "C.P3", "C.get_P3", "C.P4", "C.get_P4", "C.set_P4"], + expectedDiagnostics: + [ + // (3,5): error CS9362: 'C.P1.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // P1 = null, + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "P1").WithArguments("C.P1.set").WithLocation(3, 5), + // (5,5): error CS9362: 'C.P3.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // P3 = null, + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "P3").WithArguments("C.P3.set").WithLocation(5, 5), + // (10,5): error CS9362: 'C.P1.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // P1 = { }, + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "P1").WithArguments("C.P1.get").WithLocation(10, 5), + // (11,5): error CS9362: 'C.P2.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // P2 = { }, + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "P2").WithArguments("C.P2.get").WithLocation(11, 5), + ]); + } + + [Fact] + public void Member_Property_Pattern() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int this[int i] => 0; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int Length => 0; + } + """, + caller: """ + var c = new C(); + _ = c is { Length: 0 }; + _ = c is []; + unsafe { _ = c is { Length: 0 }; } + unsafe { _ = c is []; } + """, + additionalSources: [TestSources.Index, RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.this[]", "C.get_Item", "C.Length", "C.get_Length"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (2,12): error CS9362: 'C.Length.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = c is { Length: 0 }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "Length:").WithArguments("C.Length.get").WithLocation(2, 12), + // (3,10): error CS9362: 'C.Length.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = c is []; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "[]").WithArguments("C.Length.get").WithLocation(3, 10), + // (3,10): error CS9362: 'C.Length.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = c is []; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "[]").WithArguments("C.Length.get").WithLocation(3, 10), + // (3,10): error CS9362: 'C.this[int].get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = c is []; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "[]").WithArguments("C.this[int].get").WithLocation(3, 10), + ]); + } + + [Fact] + public void Member_Property_Extension() + { + CompileAndVerifyUnsafe( + lib: """ + public static class E + { + extension(int x) + { + public int P1 { get => x; set { } } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int P2 { get => x; set { } } + } + } + """, + caller: """ + var x = 111; + x.P1 = x.P1 + 222; + x.P2 = x.P2 + 333; + E.get_P1(x); E.set_P1(x, 0); + E.get_P2(x); E.set_P2(x, 0); + unsafe { x.P2 = x.P2 + 444; } + unsafe { E.get_P2(x); E.set_P2(x, 0); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: [ExtensionMember("E", "P2"), "E.get_P2", ExtensionMember("E", "get_P2"), "E.set_P2", ExtensionMember("E", "set_P2")], + expectedSafeSymbols: [ExtensionMember("E", "P1"), "E.get_P1", ExtensionMember("E", "get_P1"), "E.set_P1", ExtensionMember("E", "set_P1")], + expectedNoAttributeInSource: ["E.get_P2", "E.set_P2"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'E.extension(int).P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // x.P2 = x.P2 + 333; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "x.P2").WithArguments("E.extension(int).P2.set").WithLocation(3, 1), + // (3,8): error CS9362: 'E.extension(int).P2.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // x.P2 = x.P2 + 333; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "x.P2").WithArguments("E.extension(int).P2.get").WithLocation(3, 8), + // (5,1): error CS9362: 'E.get_P2(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E.get_P2(x); E.set_P2(x, 0); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E.get_P2(x)").WithArguments("E.get_P2(int)").WithLocation(5, 1), + // (5,14): error CS9362: 'E.set_P2(int, int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E.get_P2(x); E.set_P2(x, 0); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E.set_P2(x, 0)").WithArguments("E.set_P2(int, int)").WithLocation(5, 14), + ]); + } + + [Fact] + public void Member_Property_Record() + { + CompileAndVerifyUnsafe( + lib: """ + public record C(int P1, int P2) + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int P2 { get; set; } = P2; + } + """, + caller: """ + var c = new C(1, 2); + c.P2 = c.P1 + c.P2; + unsafe { c.P2 = c.P1 + c.P2; } + """, + additionalSources: [IsExternalInitTypeDefinition, RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["C.P2", "C.get_P2", "C.set_P2"], + expectedSafeSymbols: ["C.P1", "C.get_P1", "C.set_P1"], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'C.P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = c.P1 + c.P2; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C.P2.set").WithLocation(2, 1), + // (2,15): error CS9362: 'C.P2.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = c.P1 + c.P2; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C.P2.get").WithLocation(2, 15), + ]); + } + + [Fact] + public void Member_Property_Accessors() + { + var lib = """ + public class C + { + public int P1 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + public int P2 { get; [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set; } + } + """; + + CompileAndVerifyUnsafe( + lib: lib, + caller: """ + var c = new C(); + c.P1 = c.P1 + 123; + c.P2 = c.P2 + 123; + unsafe { c.P1 = c.P1 + 123; } + unsafe { c.P2 = c.P2 + 123; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.get_P1", "C.set_P2"], + expectedSafeSymbols: ["C.P1", "C.P2", "C.get_P2", "C.set_P1"], + expectedDiagnostics: + [ + // (2,8): error CS9362: 'C.P1.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P1 = c.P1 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P1").WithArguments("C.P1.get").WithLocation(2, 8), + // (3,1): error CS9362: 'C.P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = c.P2 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C.P2.set").WithLocation(3, 1), + ]); + + var expectedDiagnostics = new[] + { + // (3,22): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // public int P1 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(3, 22), + // (4,27): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // public int P2 { get; [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set; } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(4, 27), + }; + + CreateCompilation([lib, RequiresUnsafeAttributeDefinition], parseOptions: TestOptions.Regular14).VerifyEmitDiagnostics(expectedDiagnostics); + CreateCompilation([lib, RequiresUnsafeAttributeDefinition], parseOptions: TestOptions.RegularNext).VerifyEmitDiagnostics(expectedDiagnostics); + CreateCompilation([lib, RequiresUnsafeAttributeDefinition], parseOptions: TestOptions.RegularPreview).VerifyEmitDiagnostics(expectedDiagnostics); + } + + [Fact] + public void Member_Property_Field() + { + CreateCompilation( + [ + """ + class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + int P1 => field; + + [field: System.Diagnostics.CodeAnalysis.RequiresUnsafe] + int P2 => field; + } + """, + RequiresUnsafeAttributeDefinition, + ], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (6,13): error CS0592: Attribute 'System.Diagnostics.CodeAnalysis.RequiresUnsafe' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [field: System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafe", "constructor, method, property, indexer, event").WithLocation(6, 13)); + } + + [Fact] + public void Member_Property_Attribute() + { + CompileAndVerifyUnsafe( + lib: """ + public class A : System.Attribute + { + public int P1 { get; set; } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int P2 { get; set; } + public int P3 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + public int P4 { get; [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set; } + public unsafe int F; + } + """, + caller: """ + var c = new C1(); + [A(P1 = 0, P2 = 0, P3 = 0, P4 = 0, F = 0)] class C1; + [A(P1 = 0, P2 = 0, P3 = 0, P4 = 0, F = 0)] unsafe class C2; + partial class C3 + { + [A(P1 = 0, P2 = 0, P3 = 0, P4 = 0, F = 0)] void M1() { } + } + unsafe partial class C3 + { + [A(P1 = 0, P2 = 0, P3 = 0, P4 = 0, F = 0)] void M2() { } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["A.P2", "A.get_P2", "A.set_P2", "A.get_P3", "A.set_P4"], + expectedSafeSymbols: ["A.P1", "A.get_P1", "A.set_P1", "A.set_P3", "A.get_P4", "A.F"], + expectedDiagnostics: + [ + // (2,12): error CS9362: 'A.P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // [A(P1 = 0, P2 = 0, P3 = 0, P4 = 0, F = 0)] class C1; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "P2 = 0").WithArguments("A.P2.set").WithLocation(2, 12), + // (2,28): error CS9362: 'A.P4.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // [A(P1 = 0, P2 = 0, P3 = 0, P4 = 0, F = 0)] class C1; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "P4 = 0").WithArguments("A.P4.set").WithLocation(2, 28), + // (6,16): error CS9362: 'A.P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // [A(P1 = 0, P2 = 0, P3 = 0, P4 = 0, F = 0)] void M1() { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "P2 = 0").WithArguments("A.P2.set").WithLocation(6, 16), + // (6,32): error CS9362: 'A.P4.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // [A(P1 = 0, P2 = 0, P3 = 0, P4 = 0, F = 0)] void M1() { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "P4 = 0").WithArguments("A.P4.set").WithLocation(6, 32), + ]); + } + + [Fact] + public void Member_Property_Override() + { + CompileAndVerifyUnsafe( + lib: """ + public class B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public virtual int P1 { get; set; } + public virtual int P2 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + public virtual int P3 { get; set; } + } + """, + caller: """ + var c = new C1(); + c.P1 = c.P1 + 123; + c.P2 = c.P2 + 123; + c.P3 = c.P3 + 123; + + class C1 : B + { + public override int P1 { get; set; } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override int P2 { get; set; } + public override int P3 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + } + + class C2 : B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override int P1 { get; set; } + public override int P2 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override int P3 { get; set; } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["B.P1", "B.get_P1", "B.set_P1", "B.get_P2"], + expectedSafeSymbols: ["B.P2", "B.set_P2", "B.P3", "B.get_P3", "B.set_P3"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'C1.P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = c.P2 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C1.P2.set").WithLocation(3, 1), + // (3,8): error CS9362: 'C1.P2.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = c.P2 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C1.P2.get").WithLocation(3, 8), + // (4,8): error CS9362: 'C1.P3.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P3 = c.P3 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P3").WithArguments("C1.P3.get").WithLocation(4, 8), + // (10,35): error CS9364: Unsafe member 'C1.P2.set' cannot override safe member 'B.P2.set' + // public override int P2 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "set").WithArguments("C1.P2.set", "B.P2.set").WithLocation(10, 35), + // (11,79): error CS9364: Unsafe member 'C1.P3.get' cannot override safe member 'B.P3.get' + // public override int P3 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "get").WithArguments("C1.P3.get", "B.P3.get").WithLocation(11, 79), + // (20,30): error CS9364: Unsafe member 'C2.P3.get' cannot override safe member 'B.P3.get' + // public override int P3 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "get").WithArguments("C2.P3.get", "B.P3.get").WithLocation(20, 30), + // (20,35): error CS9364: Unsafe member 'C2.P3.set' cannot override safe member 'B.P3.set' + // public override int P3 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "set").WithArguments("C2.P3.set", "B.P3.set").WithLocation(20, 35), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (3,1): error CS9362: 'C1.P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = c.P2 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C1.P2.set").WithLocation(3, 1), + // (3,8): error CS9362: 'C1.P2.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = c.P2 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C1.P2.get").WithLocation(3, 8), + // (4,8): error CS9362: 'C1.P3.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P3 = c.P3 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P3").WithArguments("C1.P3.get").WithLocation(4, 8), + // (10,30): error CS9364: Unsafe member 'C1.P2.get' cannot override safe member 'B.P2.get' + // public override int P2 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "get").WithArguments("C1.P2.get", "B.P2.get").WithLocation(10, 30), + // (10,35): error CS9364: Unsafe member 'C1.P2.set' cannot override safe member 'B.P2.set' + // public override int P2 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "set").WithArguments("C1.P2.set", "B.P2.set").WithLocation(10, 35), + // (11,79): error CS9364: Unsafe member 'C1.P3.get' cannot override safe member 'B.P3.get' + // public override int P3 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "get").WithArguments("C1.P3.get", "B.P3.get").WithLocation(11, 79), + // (17,30): error CS9364: Unsafe member 'C2.P1.get' cannot override safe member 'B.P1.get' + // public override int P1 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "get").WithArguments("C2.P1.get", "B.P1.get").WithLocation(17, 30), + // (17,35): error CS9364: Unsafe member 'C2.P1.set' cannot override safe member 'B.P1.set' + // public override int P1 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "set").WithArguments("C2.P1.set", "B.P1.set").WithLocation(17, 35), + // (18,79): error CS9364: Unsafe member 'C2.P2.get' cannot override safe member 'B.P2.get' + // public override int P2 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "get").WithArguments("C2.P2.get", "B.P2.get").WithLocation(18, 79), + // (20,30): error CS9364: Unsafe member 'C2.P3.get' cannot override safe member 'B.P3.get' + // public override int P3 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "get").WithArguments("C2.P3.get", "B.P3.get").WithLocation(20, 30), + // (20,35): error CS9364: Unsafe member 'C2.P3.set' cannot override safe member 'B.P3.set' + // public override int P3 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "set").WithArguments("C2.P3.set", "B.P3.set").WithLocation(20, 35), + ]); + } + + [Fact] + public void Member_Property_Implementation() + { + CompileAndVerifyUnsafe( + lib: """ + public interface I + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + int P1 { get; set; } + int P2 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + int P3 { get; set; } + } + """, + caller: """ + I i = new C1(); + i.P1 = i.P1 + 123; + i.P2 = i.P2 + 123; + i.P3 = i.P3 + 123; + + class C1 : I + { + public int P1 { get; set; } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int P2 { get; set; } + public int P3 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + } + + class C2 : I + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int P1 { get; set; } + public int P2 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int P3 { get; set; } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["I.P1", "I.get_P1", "I.set_P1", "I.get_P2"], + expectedSafeSymbols: ["I.P2", "I.set_P2", "I.P3", "I.get_P3", "I.set_P3"], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'I.P1.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // i.P1 = i.P1 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "i.P1").WithArguments("I.P1.set").WithLocation(2, 1), + // (2,8): error CS9362: 'I.P1.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // i.P1 = i.P1 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "i.P1").WithArguments("I.P1.get").WithLocation(2, 8), + // (3,8): error CS9362: 'I.P2.get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // i.P2 = i.P2 + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "i.P2").WithArguments("I.P2.get").WithLocation(3, 8), + // (10,26): error CS9365: Unsafe member 'C1.P2.set' cannot implicitly implement safe member 'I.P2.set' + // public int P2 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "set").WithArguments("C1.P2.set", "I.P2.set").WithLocation(10, 26), + // (11,70): error CS9365: Unsafe member 'C1.P3.get' cannot implicitly implement safe member 'I.P3.get' + // public int P3 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "get").WithArguments("C1.P3.get", "I.P3.get").WithLocation(11, 70), + // (20,21): error CS9365: Unsafe member 'C2.P3.get' cannot implicitly implement safe member 'I.P3.get' + // public int P3 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "get").WithArguments("C2.P3.get", "I.P3.get").WithLocation(20, 21), + // (20,26): error CS9365: Unsafe member 'C2.P3.set' cannot implicitly implement safe member 'I.P3.set' + // public int P3 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "set").WithArguments("C2.P3.set", "I.P3.set").WithLocation(20, 26), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (10,21): error CS9365: Unsafe member 'C1.P2.get' cannot implicitly implement safe member 'I.P2.get' + // public int P2 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "get").WithArguments("C1.P2.get", "I.P2.get").WithLocation(10, 21), + // (10,26): error CS9365: Unsafe member 'C1.P2.set' cannot implicitly implement safe member 'I.P2.set' + // public int P2 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "set").WithArguments("C1.P2.set", "I.P2.set").WithLocation(10, 26), + // (11,70): error CS9365: Unsafe member 'C1.P3.get' cannot implicitly implement safe member 'I.P3.get' + // public int P3 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "get").WithArguments("C1.P3.get", "I.P3.get").WithLocation(11, 70), + // (17,21): error CS9365: Unsafe member 'C2.P1.get' cannot implicitly implement safe member 'I.P1.get' + // public int P1 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "get").WithArguments("C2.P1.get", "I.P1.get").WithLocation(17, 21), + // (17,26): error CS9365: Unsafe member 'C2.P1.set' cannot implicitly implement safe member 'I.P1.set' + // public int P1 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "set").WithArguments("C2.P1.set", "I.P1.set").WithLocation(17, 26), + // (18,70): error CS9365: Unsafe member 'C2.P2.get' cannot implicitly implement safe member 'I.P2.get' + // public int P2 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "get").WithArguments("C2.P2.get", "I.P2.get").WithLocation(18, 70), + // (20,21): error CS9365: Unsafe member 'C2.P3.get' cannot implicitly implement safe member 'I.P3.get' + // public int P3 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "get").WithArguments("C2.P3.get", "I.P3.get").WithLocation(20, 21), + // (20,26): error CS9365: Unsafe member 'C2.P3.set' cannot implicitly implement safe member 'I.P3.set' + // public int P3 { get; set; } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "set").WithArguments("C2.P3.set", "I.P3.set").WithLocation(20, 26), + ]); + } + + [Fact] + public void Member_Indexer() + { + CompileAndVerifyUnsafe( + lib: """ + public class C1 + { + public int this[int i] { get => i; set { } } + } + public class C2 + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int this[int i] { get => i; set { } } + } + """, + caller: """ + var c1 = new C1(); + c1[0] = c1[0] + 123; + var c2 = new C2(); + c2[0] = c2[0] + 123; + unsafe { c2[0] = c2[0] + 123; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C2.this[]", "C2.get_Item", "C2.set_Item"], + expectedSafeSymbols: ["C1.this[]", "C1.get_Item", "C1.set_Item"], + expectedDiagnostics: + [ + // (4,1): error CS9362: 'C2.this[int].set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c2[0] = c2[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c2[0]").WithArguments("C2.this[int].set").WithLocation(4, 1), + // (4,9): error CS9362: 'C2.this[int].get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c2[0] = c2[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c2[0]").WithArguments("C2.this[int].get").WithLocation(4, 9), + ]); + } + + [Fact] + public void Member_Indexer_CompoundAssignment() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public int this[int i] { get => i; set { } } + } + """, + caller: """ + var c = new C(); + c[0] += 123; + unsafe { c[0] += 123; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.this[]", "C.get_Item", "C.set_Item"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'C.this[int].set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c[0] += 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c[0]").WithArguments("C.this[int].set").WithLocation(2, 1), + // (2,1): error CS9362: 'C.this[int].get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c[0] += 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c[0]").WithArguments("C.this[int].get").WithLocation(2, 1), + ]); + } + + [Fact] + public void Member_Indexer_ObjectInitializer() + { + CompileAndVerifyUnsafe( + lib: """ + public class C1 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] public object this[int i] { get => null; set { } } } + public class C2 { public object this[int i] { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get => null; set { } } } + public class C3 { public object this[int i] { get => null; [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set { } } } + public class C4 { public object this[int i] { get => null; set { } } } + """, + caller: """ + _ = new C1 { [0] = null, [0] = { } }; + _ = new C2 { [0] = null, [0] = { } }; + _ = new C3 { [0] = null, [0] = { } }; + _ = new C4 { [0] = null, [0] = { } }; + unsafe { _ = new C1 { [0] = null, [0] = { } }; } + unsafe { _ = new C2 { [0] = null, [0] = { } }; } + unsafe { _ = new C3 { [0] = null, [0] = { } }; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C1.this[]", "C1.get_Item", "C1.set_Item", "C2.get_Item", "C3.set_Item"], + expectedSafeSymbols: ["C1", "C2", "C2.this[]", "C2.set_Item", "C3", "C3.this[]", "C3.get_Item", "C4", "C4.this[]", "C4.get_Item", "C4.set_Item"], + expectedDiagnostics: + [ + // (1,14): error CS9362: 'C1.this[int].set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C1 { [0] = null, [0] = { } }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "[0]").WithArguments("C1.this[int].set").WithLocation(1, 14), + // (1,26): error CS9362: 'C1.this[int].get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C1 { [0] = null, [0] = { } }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "[0]").WithArguments("C1.this[int].get").WithLocation(1, 26), + // (2,26): error CS9362: 'C2.this[int].get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C2 { [0] = null, [0] = { } }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "[0]").WithArguments("C2.this[int].get").WithLocation(2, 26), + // (3,14): error CS9362: 'C3.this[int].set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C3 { [0] = null, [0] = { } }; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "[0]").WithArguments("C3.this[int].set").WithLocation(3, 14), + ]); + } + + [Fact] + public void Member_Indexer_Accessors() + { + var lib = """ + public class C1 + { + public int this[int i] { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get => i; set { } } + } + public class C2 + { + public int this[int i] { get => i; [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set { } } + } + """; + + CompileAndVerifyUnsafe( + lib: lib, + caller: """ + var c1 = new C1(); + c1[0] = c1[0] + 123; + var c2 = new C2(); + c2[0] = c2[0] + 123; + unsafe { c1[0] = c1[0] + 123; } + unsafe { c2[0] = c2[0] + 123; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C1.get_Item", "C2.set_Item"], + expectedSafeSymbols: ["C1.this[]", "C2.this[]", "C2.get_Item", "C1.set_Item"], + expectedDiagnostics: + [ + // (2,9): error CS9362: 'C1.this[int].get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c1[0] = c1[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c1[0]").WithArguments("C1.this[int].get").WithLocation(2, 9), + // (4,1): error CS9362: 'C2.this[int].set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c2[0] = c2[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c2[0]").WithArguments("C2.this[int].set").WithLocation(4, 1), + ]); + + var expectedDiagnostics = new[] + { + // (3,31): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // public int this[int i] { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] get => i; set { } } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(3, 31), + // (7,41): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // public int this[int i] { get => i; [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set { } } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(7, 41), + }; + + CreateCompilation([lib, RequiresUnsafeAttributeDefinition], parseOptions: TestOptions.RegularNext).VerifyEmitDiagnostics(expectedDiagnostics); + CreateCompilation([lib, RequiresUnsafeAttributeDefinition], parseOptions: TestOptions.RegularPreview).VerifyEmitDiagnostics(expectedDiagnostics); + } + + [Fact] + public void Member_Event() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public event System.Action E1 { add { } remove { } } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public event System.Action E2 { add { } remove { } } + } + """, + caller: """ + var c = new C(); + c.E1 += null; + c.E2 += null; + unsafe { c.E2 += null; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.E2", "C.add_E2", "C.remove_E2"], + expectedSafeSymbols: ["C.E1", "C.add_E1", "C.remove_E1"], + expectedDiagnostics: + [ + // (3,6): error CS9362: 'C.E2.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.E2 += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("C.E2.add").WithLocation(3, 6), + ]); + + var source = """ + class C + { + event System.Action E1, E2; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] event System.Action E3, E4; + void M() + { + E1(); + E2(); + E3(); + E4(); + + E1 = null; + E2 = null; + E3 = null; + E4 = null; + + unsafe { E3(); } + unsafe { E4(); } + unsafe { E3 = null; } + unsafe { E4 = null; } + } + } + """; + CreateCompilation([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (9,9): error CS9362: 'C.E3' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E3(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E3").WithArguments("C.E3").WithLocation(9, 9), + // (10,9): error CS9362: 'C.E4' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E4(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E4").WithArguments("C.E4").WithLocation(10, 9), + // (14,9): error CS9362: 'C.E3' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E3 = null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E3").WithArguments("C.E3").WithLocation(14, 9), + // (15,9): error CS9362: 'C.E4' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E4 = null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E4").WithArguments("C.E4").WithLocation(15, 9)); + } + + [Fact] + public void Member_Event_Accessors() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public event System.Action E1 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] add { } remove { } } + public event System.Action E2 { add { } [System.Diagnostics.CodeAnalysis.RequiresUnsafe] remove { } } + } + """, + caller: """ + var c = new C(); + c.E1 += null; c.E1 -= null; + c.E2 += null; c.E2 -= null; + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.add_E1", "C.remove_E2"], + expectedSafeSymbols: ["C.E1", "C.remove_E1", "C.E2", "C.add_E2"], + expectedDiagnostics: + [ + // (2,6): error CS9362: 'C.E1.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.E1 += null; c.E1 -= null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("C.E1.add").WithLocation(2, 6), + // (3,20): error CS9362: 'C.E2.remove' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.E2 += null; c.E2 -= null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "-=").WithArguments("C.E2.remove").WithLocation(3, 20), + ]); + } + + [Fact] + public void Member_Event_Override() + { + CompileAndVerifyUnsafe( + lib: """ + #pragma warning disable CS0067 // unused event + public class B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public virtual event System.Action E1; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public virtual event System.Action E2 { add { } remove { } } + public virtual event System.Action E3; + public virtual event System.Action E4 { add { } remove { } } + } + """, + caller: """ + var c = new C1(); + c.E1 += null; + c.E2 += null; + c.E3 += null; + c.E4 += null; + + #pragma warning disable CS0067 // unused event + + class C1 : B + { + public override event System.Action E1; + public override event System.Action E2 { add { } remove { } } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override event System.Action E3; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override event System.Action E4 { add { } remove { } } + } + + class C2 : B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override event System.Action E1; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public override event System.Action E2 { add { } remove { } } + public override event System.Action E3; + public override event System.Action E4 { add { } remove { } } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["B.E1", "B.add_E1", "B.remove_E1", "B.E2", "B.add_E2", "B.remove_E2"], + expectedSafeSymbols: ["B.E3", "B.add_E3", "B.remove_E3", "B.E4", "B.add_E4", "B.remove_E4"], + expectedDiagnostics: + [ + // (4,6): error CS9362: 'C1.E3.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.E3 += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("C1.E3.add").WithLocation(4, 6), + // (5,6): error CS9362: 'C1.E4.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.E4 += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("C1.E4.add").WithLocation(5, 6), + // (14,41): error CS9364: Unsafe member 'C1.E3' cannot override safe member 'B.E3' + // public override event System.Action E3; + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "E3").WithArguments("C1.E3", "B.E3").WithLocation(14, 41), + // (16,41): error CS9364: Unsafe member 'C1.E4' cannot override safe member 'B.E4' + // public override event System.Action E4 { add { } remove { } } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "E4").WithArguments("C1.E4", "B.E4").WithLocation(16, 41), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (4,6): error CS9362: 'C1.E3.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.E3 += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("C1.E3.add").WithLocation(4, 6), + // (5,6): error CS9362: 'C1.E4.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.E4 += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("C1.E4.add").WithLocation(5, 6), + // (14,41): error CS9364: Unsafe member 'C1.E3' cannot override safe member 'B.E3' + // public override event System.Action E3; + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "E3").WithArguments("C1.E3", "B.E3").WithLocation(14, 41), + // (16,41): error CS9364: Unsafe member 'C1.E4' cannot override safe member 'B.E4' + // public override event System.Action E4 { add { } remove { } } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "E4").WithArguments("C1.E4", "B.E4").WithLocation(16, 41), + // (22,41): error CS9364: Unsafe member 'C2.E1' cannot override safe member 'B.E1' + // public override event System.Action E1; + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "E1").WithArguments("C2.E1", "B.E1").WithLocation(22, 41), + // (24,41): error CS9364: Unsafe member 'C2.E2' cannot override safe member 'B.E2' + // public override event System.Action E2 { add { } remove { } } + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "E2").WithArguments("C2.E2", "B.E2").WithLocation(24, 41), + ]); + } + + [Fact] + public void Member_Event_Implementation() + { + CompileAndVerifyUnsafe( + lib: """ + public interface I + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + event System.Action E1; + event System.Action E2; + } + """, + caller: """ + I i = new C1(); + i.E1 += null; + i.E2 += null; + + #pragma warning disable CS0067 // unused event + + class C1 : I + { + public event System.Action E1; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public event System.Action E2; + } + + class C2 : I + { + public event System.Action E1 { add { } remove { } } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public event System.Action E2 { add { } remove { } } + } + + class C3 : I + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public event System.Action E1; + public event System.Action E2; + } + + class C4 : I + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public event System.Action E1 { add { } remove { } } + public event System.Action E2 { add { } remove { } } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["I.E1", "I.add_E1", "I.remove_E1"], + expectedSafeSymbols: ["I.E2", "I.add_E2", "I.remove_E2"], + expectedDiagnostics: + [ + // (2,6): error CS9362: 'I.E1.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // i.E1 += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("I.E1.add").WithLocation(2, 6), + // (11,32): error CS9365: Unsafe member 'C1.E2' cannot implicitly implement safe member 'I.E2' + // public event System.Action E2; + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "E2").WithArguments("C1.E2", "I.E2").WithLocation(11, 32), + // (18,32): error CS9365: Unsafe member 'C2.E2' cannot implicitly implement safe member 'I.E2' + // public event System.Action E2 { add { } remove { } } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "E2").WithArguments("C2.E2", "I.E2").WithLocation(18, 32), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (11,32): error CS9365: Unsafe member 'C1.E2' cannot implicitly implement safe member 'I.E2' + // public event System.Action E2; + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "E2").WithArguments("C1.E2", "I.E2").WithLocation(11, 32), + // (18,32): error CS9365: Unsafe member 'C2.E2' cannot implicitly implement safe member 'I.E2' + // public event System.Action E2 { add { } remove { } } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "E2").WithArguments("C2.E2", "I.E2").WithLocation(18, 32), + // (24,32): error CS9365: Unsafe member 'C3.E1' cannot implicitly implement safe member 'I.E1' + // public event System.Action E1; + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "E1").WithArguments("C3.E1", "I.E1").WithLocation(24, 32), + // (31,32): error CS9365: Unsafe member 'C4.E1' cannot implicitly implement safe member 'I.E1' + // public event System.Action E1 { add { } remove { } } + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "E1").WithArguments("C4.E1", "I.E1").WithLocation(31, 32), + ]); + } + + [Fact] + public void Member_Constructor() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public C(int i) { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + } + public unsafe class C2(int x) + { + int _x = x; + } + [method: System.Diagnostics.CodeAnalysis.RequiresUnsafe] public class C3(); + """, + caller: """ + _ = new C(0); + _ = new C(); + unsafe { _ = new C(); } + _ = new C2(0); + _ = new C3(); + unsafe { _ = new C3(); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: [Overload("C..ctor", parameterCount: 0)], + expectedSafeSymbols: ["C", Overload("C..ctor", parameterCount: 1), "C2", "C2..ctor"], + expectedDiagnostics: + [ + // (2,5): error CS9362: 'C.C()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C()").WithArguments("C.C()").WithLocation(2, 5), + // (5,5): error CS9362: 'C3.C3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C3(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C3()").WithArguments("C3.C3()").WithLocation(5, 5), + ]); + } + + [Fact] + public void Member_Constructor_Base() + { + CompileAndVerifyUnsafe( + lib: """ + public class B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public B() { } + } + """, + caller: """ + _ = new C1(); + _ = new C2(); + _ = new C3(); + _ = new C4(); + _ = new C5(); + _ = new D1(); + _ = new D2(); + unsafe { _ = new C5(); } + + class C1 : B; + class C2 : B + { + public C2() { } + public C2(int x) : base() { } + } + class D1() : B(); + + unsafe class C3 : B; + unsafe class C4 : B + { + public C4() { } + public C4(int x) : base() { } + } + unsafe class D2() : B(); + + class C5 : B + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C5() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C5(int x) : base() { } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["B..ctor"], + expectedSafeSymbols: ["B"], + expectedDiagnostics: + [ + // (5,5): error CS9362: 'C5.C5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C5(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C5()").WithArguments("C5.C5()").WithLocation(5, 5), + // (10,1): error CS9362: 'B.B()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // class C1 : B; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "class C1 : B;").WithArguments("B.B()").WithLocation(10, 1), + // (13,5): error CS9362: 'B.B()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public C2() { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "public C2() { }").WithArguments("B.B()").WithLocation(13, 5), + // (14,22): error CS9362: 'B.B()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public C2(int x) : base() { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, ": base()").WithArguments("B.B()").WithLocation(14, 22), + // (16,14): error CS9362: 'B.B()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // class D1() : B(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "B()").WithArguments("B.B()").WithLocation(16, 14), + // (28,5): error CS9362: 'B.B()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + // public C5() { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, @"[System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C5() { }").WithArguments("B.B()").WithLocation(28, 5), + // (31,22): error CS9362: 'B.B()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public C5(int x) : base() { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, ": base()").WithArguments("B.B()").WithLocation(31, 22), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (5,5): error CS9362: 'C5.C5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C5(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C5()").WithArguments("C5.C5()").WithLocation(5, 5), + ]); + } + + [Fact] + public void Member_Constructor_This() + { + var source = """ + class C + { + public C() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C(int x) { } + public C(string s) : this() { } + public C(C c) : this(0) { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C(int[] a) : this(0) { } + } + """; + CreateCompilation([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (7,19): error CS9362: 'C.C(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public C(C c) : this(0) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, ": this(0)").WithArguments("C.C(int)").WithLocation(7, 19), + // (9,23): error CS9362: 'C.C(int)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public C(int[] a) : this(0) { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, ": this(0)").WithArguments("C.C(int)").WithLocation(9, 23)); + } + + [Fact] + public void Member_Constructor_Static() + { + CreateCompilation( + [ + """ + public class C + { + public static readonly int F = 42; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + static C() { } + } + """, + RequiresUnsafeAttributeDefinition, + ], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (4,6): error CS9367: RequiresUnsafeAttribute cannot be applied to this symbol. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_RequiresUnsafeAttributeUnsupportedMemberTarget, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(4, 6)); + } + + [Fact] + public void Member_Constructor_Attribute() + { + CompileAndVerifyUnsafe( + lib: """ + public class A : System.Attribute + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public A() { } + public A(int x) { } + } + """, + caller: """ + #pragma warning disable CS8321 // unused local function + [A] void M1() { } + [A(0)] void M2() { } + unsafe { [A] void M3() { } } + [A] unsafe void M4() { } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: [Overload("A..ctor", parameterCount: 0)], + expectedSafeSymbols: ["A", Overload("A..ctor", parameterCount: 1)], + expectedDiagnostics: + [ + // (2,2): error CS9362: 'A.A()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // [A] void M1() { } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "A").WithArguments("A.A()").WithLocation(2, 2), + ]); + } + + [Fact] + public void Member_Constructor_NewConstraint() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + public static void M() where T : new() { } + } + public class D where T : new(); + """, + caller: """ + using X = D; + C.M(); + _ = new D(); + _ = new X(); + unsafe { C.M(); } + unsafe { _ = new D(); } + unsafe { _ = new X(); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C", "C.M", "D", "D..ctor"], + expectedDiagnostics: + [ + // (1,7): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D' + // using X = D; + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "X").WithArguments("C.C()", "T", "D").WithLocation(1, 7), + // (2,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'C.M()' + // C.M(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "C.M()").WithArguments("C.C()", "T", "C.M()").WithLocation(2, 1), + // (3,9): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D' + // _ = new D(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "D").WithArguments("C.C()", "T", "D").WithLocation(3, 9), + ]); + } + + [Fact] + public void Member_Constructor_NewConstraint_ExtensionMember() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + } + """, + caller: """ + var c = new C(); + _ = c.P1; + _ = C.P2; + + static class E + { + extension(T t) where T : new() + { + public int P1 => new T().GetHashCode(); + } + extension(T) where T : new() + { + public static int P2 => new T().GetHashCode(); + } + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,9): error CS9502: 'C.C()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var c = new C(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C()").WithArguments("C.C()").WithLocation(1, 9), + // (2,5): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'E.extension(T).P1.get' + // _ = c.P1; + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "c.P1").WithArguments("C.C()", "T", "E.extension(T).P1.get").WithLocation(2, 5), + // (3,5): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'E.extension(T).P2.get' + // _ = C.P2; + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "C.P2").WithArguments("C.C()", "T", "E.extension(T).P2.get").WithLocation(3, 5), + ]); + } + + [Fact] + public void Member_Constructor_NewConstraint_MoreArguments() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + } + """, + caller: """ + M1(); + M1(); + M2(null, null, null); + + C c = null; + M2(c, c, c); + + static void M1() where T2 : new() { } + static void M2(T1 t1, T2 t2, T3 t3) where T1 : new() where T3 : new() { } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (2,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T2' in 'M1()' + // M1(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "M1()").WithArguments("C.C()", "T2", "M1()").WithLocation(2, 1), + // (3,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T1' in 'M2(T1, T2, T3)' + // M2(null, null, null); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "M2(null, null, null)").WithArguments("C.C()", "T1", "M2(T1, T2, T3)").WithLocation(3, 1), + // (3,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T3' in 'M2(T1, T2, T3)' + // M2(null, null, null); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "M2(null, null, null)").WithArguments("C.C()", "T3", "M2(T1, T2, T3)").WithLocation(3, 1), + // (6,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T1' in 'M2(T1, T2, T3)' + // M2(c, c, c); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "M2(c, c, c)").WithArguments("C.C()", "T1", "M2(T1, T2, T3)").WithLocation(6, 1), + // (6,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T3' in 'M2(T1, T2, T3)' + // M2(c, c, c); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "M2(c, c, c)").WithArguments("C.C()", "T3", "M2(T1, T2, T3)").WithLocation(6, 1), + ]); + } + + [Fact] + public void Member_Constructor_NewConstraint_MoreConstructors() + { + CompileAndVerifyUnsafe( + lib: """ + public class C1 + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C1(int x) { } + public C1() { } + } + [method: System.Diagnostics.CodeAnalysis.RequiresUnsafe] public class C2(); + """, + caller: """ + M(); + M(); + static void M() where T : new() { } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: [Overload("C1..ctor", parameterCount: 1), "C2..ctor"], + expectedSafeSymbols: [Overload("C1..ctor", parameterCount: 0)], + expectedDiagnostics: + [ + // (2,1): error CS9376: An unsafe context is required for constructor 'C2.C2()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'M()' + // M(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "M()").WithArguments("C2.C2()", "T", "M()").WithLocation(2, 1), + ]); + } + + [Fact] + public void Member_Constructor_NewConstraint_Using() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + } + """, + caller: """ + using static D1; + using static unsafe D2; + using X1 = D1; + using unsafe X2 = D2; + + _ = new X1(); + X1 x1 = new(); + X1.M1(); + M1(); + D1.M1(); + + _ = new X2(); + X2 x2 = new(); + X2.M2(); + M2(); + D2.M2(); + + class D1 where T : new() { public static void M1() { } } + class D2 where T : new() { public static void M2() { } } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,14): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D1' + // using static D1; + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "D1").WithArguments("C.C()", "T", "D1").WithLocation(1, 14), + // (3,7): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D1' + // using X1 = D1; + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "X1").WithArguments("C.C()", "T", "D1").WithLocation(3, 7), + // (10,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D1' + // D1.M1(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "D1").WithArguments("C.C()", "T", "D1").WithLocation(10, 1), + // (16,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D2' + // D2.M2(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "D2").WithArguments("C.C()", "T", "D2").WithLocation(16, 1), + ]); + } + + [Fact] + public void Member_Constructor_NewConstraint_Namespace() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + } + namespace N + { + public class D where T : new(); + } + """, + caller: """ + N.D x = new N.D(); + unsafe { N.D y = new N.D(); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C", "N.D", "N.D..ctor"], + expectedDiagnostics: + [ + // (1,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D' + // N.D x = new N.D(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "N.D").WithArguments("C.C()", "T", "N.D").WithLocation(1, 1), + // (1,16): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D' + // N.D x = new N.D(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "N.D").WithArguments("C.C()", "T", "N.D").WithLocation(1, 16), + ]); + } + + [Fact] + public void Member_Constructor_NewConstraint_NestedType_01() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + } + public class D where T : new() + { + public class Nested; + } + """, + caller: """ + D.Nested x = new D.Nested(); + unsafe { D.Nested y = new D.Nested(); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C", "D", "D..ctor", "D.Nested", "D.Nested..ctor"], + expectedDiagnostics: + [ + // (1,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D' + // D.Nested x = new D.Nested(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "D").WithArguments("C.C()", "T", "D").WithLocation(1, 1), + // (1,21): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D' + // D.Nested x = new D.Nested(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "D").WithArguments("C.C()", "T", "D").WithLocation(1, 21), + ]); + } + + [Fact] + public void Member_Constructor_NewConstraint_NestedType_02() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + } + public class D + { + public class Nested where T : new(); + } + """, + caller: """ + D.Nested x = new D.Nested(); + unsafe { D.Nested y = new D.Nested(); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C", "D", "D..ctor", "D.Nested", "D.Nested..ctor"], + expectedDiagnostics: + [ + // (1,1): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D.Nested' + // D.Nested x = new D.Nested(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "D.Nested").WithArguments("C.C()", "T", "D.Nested").WithLocation(1, 1), + // (1,21): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'D.Nested' + // D.Nested x = new D.Nested(); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "D.Nested").WithArguments("C.C()", "T", "D.Nested").WithLocation(1, 21), + ]); + } + + [Fact] + public void Member_Constructor_NewConstraint_ParameterlessStruct() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public C() { } + } + public struct S where T : new(); + """, + caller: """ + #pragma warning disable CS0219, CS0169 // unused variable, field + var s = M(new S()); + M(new()); + M(default); + M(default(S)); + M(s with { }); + X x = new(); + + unsafe S M(S s) => s; + + class X { S f; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C", "S", "S..ctor"], + expectedDiagnostics: + [ + // (2,15): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'S' + // var s = M(new S()); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "S").WithArguments("C.C()", "T", "S").WithLocation(2, 15), + // (5,11): error CS9376: An unsafe context is required for constructor 'C.C()' marked as 'RequiresUnsafe' or 'extern' to satisfy the 'new()' constraint of type parameter 'T' in 'S' + // M(default(S)); + Diagnostic(ErrorCode.ERR_UnsafeConstructorConstraint, "S").WithArguments("C.C()", "T", "S").WithLocation(5, 11), + ]); + } + + [Fact] + public void Member_Destructor() + { + var comp = CreateCompilation( + [ + """ + class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + ~C() { } + void M() { Finalize(); } + } + class D : C + { + ~D() { } // implicitly calls base finalizer + } + """, + RequiresUnsafeAttributeDefinition, + ], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,6): error CS9367: RequiresUnsafeAttribute cannot be applied to this symbol. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_RequiresUnsafeAttributeUnsupportedMemberTarget, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(3, 6), + // (5,16): error CS0245: Destructors and object.Finalize cannot be called directly. Consider calling IDisposable.Dispose if available. + // void M() { Finalize(); } + Diagnostic(ErrorCode.ERR_CallingFinalizeDeprecated, "Finalize()").WithLocation(5, 16)); + + VerifyRequiresUnsafeAttribute( + comp.SourceModule, + expectedUnsafeSymbols: [], + expectedSafeSymbols: ["C.Finalize"], + expectedAttribute: ["C.Finalize"]); + } + + [Fact] + public void Member_Operator_Static() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public static C operator +(C c1, C c2) => c1; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static C operator -(C c1, C c2) => c1; + } + """, + caller: """ + var c = new C(); + _ = c + c; + _ = c - c; + unsafe { _ = c - c; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.op_Subtraction"], + expectedSafeSymbols: ["C.op_Addition"], + expectedDiagnostics: + [ + // (3,5): error CS9362: 'C.operator -(C, C)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = c - c; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c - c").WithArguments("C.operator -(C, C)").WithLocation(3, 5), + ]); + } + + [Fact] + public void Member_Operator_Static_Extension() + { + CompileAndVerifyUnsafe( + lib: """ + public class C; + public static class E + { + extension(C) + { + public static C operator +(C c1, C c2) => c1; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static C operator -(C c1, C c2) => c1; + } + } + """, + caller: """ + var c = new C(); + _ = c + c; + _ = c - c; + E.op_Addition(c, c); + E.op_Subtraction(c, c); + unsafe { _ = c - c; } + unsafe { E.op_Subtraction(c, c); } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["E.op_Subtraction", ExtensionMember("E", "op_Subtraction")], + expectedSafeSymbols: ["E.op_Addition", ExtensionMember("E", "op_Addition")], + expectedDiagnostics: + [ + // (3,5): error CS9362: 'E.extension(C).operator -(C, C)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = c - c; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c - c").WithArguments("E.extension(C).operator -(C, C)").WithLocation(3, 5), + // (5,1): error CS9362: 'E.op_Subtraction(C, C)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E.op_Subtraction(c, c); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E.op_Subtraction(c, c)").WithArguments("E.op_Subtraction(C, C)").WithLocation(5, 1), + ]); + } + + [Fact] + public void Member_Operator_Instance() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public void operator +=(C c) { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void operator -=(C c) { } + } + """, + caller: """ + var c = new C(); + c += c; + c -= c; + unsafe { c -= c; } + """, + additionalSources: [CompilerFeatureRequiredAttribute, RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.op_SubtractionAssignment"], + expectedSafeSymbols: ["C.op_AdditionAssignment"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'C.operator -=(C)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c -= c; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c -= c").WithArguments("C.operator -=(C)").WithLocation(3, 1), + ]); + } + + [Fact] + public void Member_Operator_Instance_Extension() + { + CompileAndVerifyUnsafe( + lib: """ + public class C; + public static class E + { + extension(C c1) + { + public void operator +=(C c2) { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void operator -=(C c2) { } + } + } + """, + caller: """ + var c = new C(); + c += c; + c -= c; + E.op_AdditionAssignment(c, c); + E.op_SubtractionAssignment(c, c); + unsafe { c -= c; } + unsafe { E.op_SubtractionAssignment(c, c); } + """, + additionalSources: [CompilerFeatureRequiredAttribute, RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["E.op_SubtractionAssignment", ExtensionMember("E", "op_SubtractionAssignment")], + expectedSafeSymbols: ["E.op_AdditionAssignment", ExtensionMember("E", "op_AdditionAssignment")], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'E.extension(C).operator -=(C)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c -= c; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c -= c").WithArguments("E.extension(C).operator -=(C)").WithLocation(3, 1), + // (5,1): error CS9362: 'E.op_SubtractionAssignment(C, C)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // E.op_SubtractionAssignment(c, c); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "E.op_SubtractionAssignment(c, c)").WithArguments("E.op_SubtractionAssignment(C, C)").WithLocation(5, 1), + ]); + } + + [Fact] + public void Member_Operator_Instance_Unary() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public void operator ++() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void operator --() { } + } + """, + caller: """ + var c = new C(); + c++; + c--; + unsafe { c--; } + """, + additionalSources: [CompilerFeatureRequiredAttribute, RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.op_DecrementAssignment"], + expectedSafeSymbols: ["C.op_IncrementAssignment"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'C.operator --()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c--; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c--").WithArguments("C.operator --()").WithLocation(3, 1), + ]); + } + + [Fact] + public void Member_Operator_Conversion() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + public static implicit operator int(C c) => 0; + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static implicit operator string(C c) => ""; + } + """, + caller: """ + var c = new C(); + int i = c; + string s = c; + unsafe { string s2 = c; } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: [OverloadByReturnType("C.op_Implicit", "System.String")], + expectedSafeSymbols: [OverloadByReturnType("C.op_Implicit", "System.Int32")], + expectedDiagnostics: + [ + // (3,12): error CS9362: 'C.implicit operator string(C)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // string s = c; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c").WithArguments("C.implicit operator string(C)").WithLocation(3, 12), + ]); + } + + [Theory, CombinatorialData] + public void Member_FunctionPointer(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public unsafe class C + { + public delegate* F; + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var c = new C(); + string s = c.F(); + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,12): error CS9360: This operation may only be used in an unsafe context + // string s = c.F(); + Diagnostic(ErrorCode.ERR_UnsafeOperation, "c.F()").WithLocation(2, 12)); + + CompileAndVerify(""" + var c = new C(); + unsafe { string s = c.F(); } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: [], + expectedSafeSymbols: ["C", "C.F", (object)getFunctionPointerType, (object)getFunctionPointerMethod], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (2,12): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // string s = c.F(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c.F()").WithLocation(2, 12)); + + static Symbol getFunctionPointerType(ModuleSymbol module) + { + return module.GlobalNamespace.GetMember("C.F").GetTypeOrReturnType().Type; + } + + static Symbol getFunctionPointerMethod(ModuleSymbol module) + { + var functionPointerType = (FunctionPointerTypeSymbol)getFunctionPointerType(module); + return functionPointerType.Signature; + } + } + + [Fact] + public void Member_Field_UnsafeInitializer() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static int M() => 0; + } + """, + caller: """ + var d = new D(); + class D + { + int F1 = *default(int*); + int F2 = C.M(); + + unsafe int U1 = *default(int*); + unsafe int U2 = C.M(); + } + unsafe class U + { + int F1 = *default(int*); + int F2 = C.M(); + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (4,14): error CS9360: This operation may only be used in an unsafe context + // int F1 = *default(int*); + Diagnostic(ErrorCode.ERR_UnsafeOperation, "*").WithLocation(4, 14), + // (5,14): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // int F2 = C.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.M()").WithArguments("C.M()").WithLocation(5, 14), + ], + expectedDiagnosticsForLegacyCaller: + [ + // (4,15): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int F1 = *default(int*); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "default(int*)").WithLocation(4, 15), + // (4,23): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int F1 = *default(int*); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(4, 23), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (4,14): error CS9360: This operation may only be used in an unsafe context + // int F1 = *default(int*); + Diagnostic(ErrorCode.ERR_UnsafeOperation, "*").WithLocation(4, 14), + ]); + } + + [Theory, CombinatorialData] + public void CompatMode_Method_ParameterType( + [CombinatorialValues("int*", "int*[]", "delegate*")] string parameterType, + bool unsafeOnType, + bool useCompilationReference) + { + var (typeModifier, methodModifier) = unsafeOnType ? ("unsafe", "") : ("", "unsafe"); + + var lib = CreateCompilation($$""" + public {{typeModifier}} class C + { + public {{methodModifier}} void M1(int x) { } + public {{methodModifier}} void M2({{parameterType}} y) { } + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var c = new C(); + c.M1(0); + c.M2(null); + unsafe { c.M2(null); } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,1): error CS9363: 'C.M2(int*)' must be used in an unsafe context because it has pointers in its signature + // c.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c.M2(null)").WithArguments($"C.M2({parameterType})").WithLocation(3, 1)); + + CompileAndVerify(""" + var c = new C(); + c.M1(0); + unsafe { c.M2(null); } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: ["C.M2"], + expectedSafeSymbols: ["C", "C.M1"], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (3,6): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "null").WithLocation(3, 6), + // (3,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c.M2(null)").WithLocation(3, 1)); + } + + [Theory, CombinatorialData] + public void CompatMode_Method_ReturnType( + [CombinatorialValues("int*", "int*[]", "delegate*")] string returnType, + bool useCompilationReference) + { + var lib = CreateCompilation($$""" + public class C + { + public unsafe int M1(int i) => i; + public unsafe {{returnType}} M2(string s) => null; + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var c = new C(); + c.M1(0); + c.M2(null); + unsafe { c.M2(null); } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,1): error CS9363: 'C.M2(string)' must be used in an unsafe context because it has pointers in its signature + // c.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c.M2(null)").WithArguments("C.M2(string)").WithLocation(3, 1)); + + CompileAndVerify(""" + var c = new C(); + c.M1(0); + unsafe { c.M2(null); } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: ["C.M2"], + expectedSafeSymbols: ["C", "C.M1"], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (3,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c.M2(null)").WithLocation(3, 1)); + } + + [Theory, CombinatorialData] + public void CompatMode_Method_ConstraintType(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public class C + { + public unsafe void M(T t) where T : I { } + } + public interface I; + public unsafe class D : I; + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var c = new C(); + c.M(null); + """; + + CompileAndVerify(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + symbolValidator: validate) + .VerifyDiagnostics(); + + CompileAndVerify(source, + [libRef], + options: TestOptions.UnsafeReleaseExe, + symbolValidator: validate) + .VerifyDiagnostics(); + + static void validate(ModuleSymbol module) + { + VerifyRequiresUnsafeAttribute( + module.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: [], + expectedSafeSymbols: ["C", "I", "C.M", "D"], + expectedUnsafeMode: CallerUnsafeMode.Implicit); + } + } + + [Theory, CombinatorialData] + public void CompatMode_Method_DefaultParameterValue(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public class C + { + public unsafe void M(string s = nameof(I)) { } + } + public interface I; + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var c = new C(); + c.M(s: null); + """; + + CompileAndVerify(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + symbolValidator: validate) + .VerifyDiagnostics(); + + CompileAndVerify(source, + [libRef], + options: TestOptions.UnsafeReleaseExe, + symbolValidator: validate) + .VerifyDiagnostics(); + + static void validate(ModuleSymbol module) + { + VerifyRequiresUnsafeAttribute( + module.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: [], + expectedSafeSymbols: ["C", "C.M", "I"]); + } + } + + [Theory, CombinatorialData] + public void CompatMode_Method_ExtensionMethod_ReceiverType(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public static class E + { + public static unsafe void M1(this int x) { } + public static unsafe void M2(this int*[] y) { } + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + 123.M1(); + new int*[0].M2(); + E.M1(123); + E.M2(null); + unsafe { new int*[0].M2(); } + unsafe { E.M2(null); } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9363: 'E.M2(int*[])' must be used in an unsafe context because it has pointers in its signature + // new int*[0].M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "new int*[0].M2()").WithArguments("E.M2(int*[])").WithLocation(2, 1), + // (4,1): error CS9363: 'E.M2(int*[])' must be used in an unsafe context because it has pointers in its signature + // E.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "E.M2(null)").WithArguments("E.M2(int*[])").WithLocation(4, 1)); + + CompileAndVerify(""" + 123.M1(); + E.M1(123); + unsafe { new int*[0].M2(); } + unsafe { E.M2(null); } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: ["E.M2"], + expectedSafeSymbols: ["E", "E.M1"], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (2,5): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].M2(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(2, 5), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].M2(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "new int*[0]").WithLocation(2, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].M2(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "new int*[0].M2()").WithLocation(2, 1), + // (4,6): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // E.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "null").WithLocation(4, 6), + // (4,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // E.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "E.M2(null)").WithLocation(4, 1)); + } + + [Theory, CombinatorialData] + public void CompatMode_Method_ExtensionMember_ReceiverType(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public unsafe static class E + { + extension(int x) + { + public void M1() { } + } + + extension(int*[] y) + { + public void M2() { } + } + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + 123.M1(); + new int*[0].M2(); + E.M1(123); + E.M2(null); + unsafe { new int*[0].M2(); } + unsafe { E.M2(null); } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9363: 'E.extension(int*[]).M2()' must be used in an unsafe context because it has pointers in its signature + // new int*[0].M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "new int*[0].M2()").WithArguments("E.extension(int*[]).M2()").WithLocation(2, 1), + // (4,1): error CS9363: 'E.M2(int*[])' must be used in an unsafe context because it has pointers in its signature + // E.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "E.M2(null)").WithArguments("E.M2(int*[])").WithLocation(4, 1)); + + CompileAndVerify(""" + 123.M1(); + E.M1(123); + unsafe { new int*[0].M2(); } + unsafe { E.M2(null); } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: ["E.M2", ExtensionMember("E", "M2")], + expectedSafeSymbols: ["E", "E.M1", ExtensionMember("E", "M1")], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (2,5): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].M2(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(2, 5), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].M2(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "new int*[0]").WithLocation(2, 1), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].M2(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "new int*[0].M2()").WithLocation(2, 1), + // (4,6): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // E.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "null").WithLocation(4, 6), + // (4,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // E.M2(null); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "E.M2(null)").WithLocation(4, 1)); + } + + [Theory, CombinatorialData] + public void CompatMode_Method_Override(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public class B + { + public unsafe virtual int* M1() => null; + public unsafe virtual int* M2() => null; + public unsafe virtual int* M3() => null; + public unsafe virtual void M4() { } + public unsafe virtual void M5() { } + public unsafe virtual void M6() { } + } + + public class C : B + { + public unsafe override int* M1() => null; + public unsafe new virtual int* M2() => null; + public unsafe override void M4() { } + public unsafe new virtual void M5() { } + } + """, + options: TestOptions.UnsafeReleaseDll) + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + CreateCompilation(""" + var d1 = new D1(); d1.M1(); d1.M2(); d1.M3(); d1.M4(); d1.M5(); d1.M6(); + var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + + class D1 : C + { + public override int* M1() => null; + public override int* M2() => null; + public override int* M3() => null; + public override void M4() { } + public override void M5() { } + public override void M6() { } + public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + } + + class D2 : C + { + public unsafe override int* M1() => null; + public unsafe override int* M2() => null; + public unsafe override int* M3() => null; + public unsafe override void M4() { } + public unsafe override void M5() { } + public unsafe override void M6() { } + } + + class D3 : C; + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,20): error CS9363: 'C.M1()' must be used in an unsafe context because it has pointers in its signature + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "d3.M1()").WithArguments("C.M1()").WithLocation(3, 20), + // (3,29): error CS9363: 'C.M2()' must be used in an unsafe context because it has pointers in its signature + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "d3.M2()").WithArguments("C.M2()").WithLocation(3, 29), + // (3,38): error CS9363: 'B.M3()' must be used in an unsafe context because it has pointers in its signature + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "d3.M3()").WithArguments("B.M3()").WithLocation(3, 38), + // (4,11): error CS9363: 'C.M1()' must be used in an unsafe context because it has pointers in its signature + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c.M1()").WithArguments("C.M1()").WithLocation(4, 11), + // (4,19): error CS9363: 'C.M2()' must be used in an unsafe context because it has pointers in its signature + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c.M2()").WithArguments("C.M2()").WithLocation(4, 19), + // (4,27): error CS9363: 'B.M3()' must be used in an unsafe context because it has pointers in its signature + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c.M3()").WithArguments("B.M3()").WithLocation(4, 27), + // (14,31): error CS9363: 'C.M1()' must be used in an unsafe context because it has pointers in its signature + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "base.M1()").WithArguments("C.M1()").WithLocation(14, 31), + // (14,42): error CS9363: 'C.M2()' must be used in an unsafe context because it has pointers in its signature + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "base.M2()").WithArguments("C.M2()").WithLocation(14, 42), + // (14,53): error CS9363: 'B.M3()' must be used in an unsafe context because it has pointers in its signature + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "base.M3()").WithArguments("B.M3()").WithLocation(14, 53)); + } + + [Theory, CombinatorialData] + public void CompatMode_Method_Implementation(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public interface I + { + unsafe int* M1(); + unsafe void M2(); + } + """, + options: TestOptions.UnsafeReleaseDll) + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + I i = new C1(); + i.M1(); + i.M2(); + + public class C1 : I + { + public int* M1() => null; + public void M2() { } + } + + public class C2 : I + { + int* I.M1() => null; + void I.M2() { } + } + + public class C3 : I + { + public unsafe int* M1() => null; + public unsafe void M2() { } + } + + public class C4 : I + { + unsafe int* I.M1() => null; + unsafe void I.M2() { } + } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9363: 'I.M1()' must be used in an unsafe context because it has pointers in its signature + // i.M1(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "i.M1()").WithArguments("I.M1()").WithLocation(2, 1)); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // i.M1(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "i.M1()").WithLocation(2, 1), + // (7,12): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public int* M1() => null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(7, 12), + // (13,5): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int* I.M1() => null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(13, 5)); + } + + [Theory, CombinatorialData] + public void CompatMode_Method_Implementation_Generic(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public interface I + { + T M1(); + void M2(); + } + """, + options: TestOptions.UnsafeReleaseDll) + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + I i = new C1(); + i.M1(); + i.M2(); + + public class C1 : I + { + public int*[] M1() => null; + public void M2() { } + } + + public class C2 : I + { + int*[] I.M1() => null; + void I.M2() { } + } + + public class C3 : I + { + public unsafe int*[] M1() => null; + public unsafe void M2() { } + } + + public class C4 : I + { + unsafe int*[] I.M1() => null; + unsafe void I.M2() { } + } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (5,21): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public class C1 : I + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(5, 21), + // (11,21): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public class C2 : I + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(11, 21), + // (23,21): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public class C4 : I + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(23, 21), + // (17,21): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public class C3 : I + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(17, 21), + // (7,12): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public int*[] M1() => null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(7, 12), + // (13,14): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int*[] I.M1() => null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(13, 14), + // (14,12): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // void I.M2() { } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(14, 12), + // (13,5): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // int*[] I.M1() => null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(13, 5), + // (1,3): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // I i = new C1(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(1, 3), + // (2,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // i.M1(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "i.M1()").WithLocation(2, 1)); + } + + [Theory, CombinatorialData] + public void CompatMode_Property( + [CombinatorialValues("int*", "int*[]", "delegate*")] string type, + bool useCompilationReference) + { + var lib = CreateCompilation($$""" + public class C + { + public unsafe int P1 { get; set; } + public unsafe {{type}} P2 { get; set; } + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var c = new C(); + c.P1 = c.P1; + c.P2 = c.P2; + unsafe { c.P2 = c.P2; } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,1): error CS9363: 'C.P2.set' must be used in an unsafe context because it has pointers in its signature + // c.P2 = c.P2; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c.P2").WithArguments("C.P2.set").WithLocation(3, 1), + // (3,8): error CS9363: 'C.P2.get' must be used in an unsafe context because it has pointers in its signature + // c.P2 = c.P2; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c.P2").WithArguments("C.P2.get").WithLocation(3, 8)); + + CompileAndVerify(""" + var c = new C(); + c.P1 = c.P1; + unsafe { c.P2 = c.P2; } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: ["C.P2", "C.get_P2", "C.set_P2"], + expectedSafeSymbols: ["C", "C.P1", "C.get_P1", "C.set_P1"], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (3,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c.P2 = c.P2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c.P2").WithLocation(3, 1), + // (3,8): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c.P2 = c.P2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c.P2").WithLocation(3, 8), + // (3,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c.P2 = c.P2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c.P2 = c.P2").WithLocation(3, 1)); + } + + [Theory, CombinatorialData] + public void CompatMode_Property_Extension_ReceiverType(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public unsafe static class E + { + extension(int x) + { + public int P1 { get => 0; set { } } + } + + extension(int*[] y) + { + public int P2 { get => 0; set { } } + } + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var x = 123; + x.P1 = x.P1; + new int*[0].P2 = new int*[0].P2; + E.get_P1(x); E.set_P1(x, 0); + E.get_P2(null); E.set_P2(null, 0); + unsafe { new int*[0].P2 = new int*[0].P2; } + unsafe { E.get_P2(null); E.set_P2(null, 0); } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,1): error CS9363: 'E.extension(int*[]).P2.set' must be used in an unsafe context because it has pointers in its signature + // new int*[0].P2 = new int*[0].P2; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "new int*[0].P2").WithArguments("E.extension(int*[]).P2.set").WithLocation(3, 1), + // (3,18): error CS9363: 'E.extension(int*[]).P2.get' must be used in an unsafe context because it has pointers in its signature + // new int*[0].P2 = new int*[0].P2; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "new int*[0].P2").WithArguments("E.extension(int*[]).P2.get").WithLocation(3, 18), + // (5,1): error CS9363: 'E.get_P2(int*[])' must be used in an unsafe context because it has pointers in its signature + // E.get_P2(null); E.set_P2(null, 0); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "E.get_P2(null)").WithArguments("E.get_P2(int*[])").WithLocation(5, 1), + // (5,17): error CS9363: 'E.set_P2(int*[], int)' must be used in an unsafe context because it has pointers in its signature + // E.get_P2(null); E.set_P2(null, 0); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "E.set_P2(null, 0)").WithArguments("E.set_P2(int*[], int)").WithLocation(5, 17)); + + CompileAndVerify(""" + var x = 123; + x.P1 = x.P1; + E.get_P1(x); E.set_P1(x, 0); + unsafe { new int*[0].P2 = new int*[0].P2; } + unsafe { E.get_P2(null); E.set_P2(null, 0); } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: [ExtensionMember("E", "P2"), "E.get_P2", ExtensionMember("E", "get_P2"), "E.set_P2", ExtensionMember("E", "set_P2")], + expectedSafeSymbols: ["E", ExtensionMember("E", "P1"), "E.get_P1", ExtensionMember("E", "get_P1"), "E.set_P1", ExtensionMember("E", "set_P1")], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (3,5): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].P2 = new int*[0].P2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(3, 5), + // (3,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].P2 = new int*[0].P2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "new int*[0]").WithLocation(3, 1), + // (3,22): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].P2 = new int*[0].P2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(3, 22), + // (3,18): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // new int*[0].P2 = new int*[0].P2; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "new int*[0]").WithLocation(3, 18), + // (5,10): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // E.get_P2(null); E.set_P2(null, 0); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "null").WithLocation(5, 10), + // (5,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // E.get_P2(null); E.set_P2(null, 0); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "E.get_P2(null)").WithLocation(5, 1), + // (5,26): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // E.get_P2(null); E.set_P2(null, 0); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "null").WithLocation(5, 26), + // (5,17): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // E.get_P2(null); E.set_P2(null, 0); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "E.set_P2(null, 0)").WithLocation(5, 17)); + } + + [Theory, CombinatorialData] + public void CompatMode_Indexer( + [CombinatorialValues("int*", "int*[]", "delegate*")] string type, + bool useCompilationReference) + { + var lib = CreateCompilation($$""" + public class C1 + { + public unsafe int this[int i] { get => i; set { } } + } + public class C2 + { + public unsafe {{type}} this[int i] { get => null; set { } } + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var c1 = new C1(); + c1[0] = c1[0]; + var c2 = new C2(); + c2[0] = c2[0]; + unsafe { c2[0] = c2[0]; } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (4,1): error CS9363: 'C2.this[int].set' must be used in an unsafe context because it has pointers in its signature + // c2[0] = c2[0]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c2[0]").WithArguments("C2.this[int].set").WithLocation(4, 1), + // (4,9): error CS9363: 'C2.this[int].get' must be used in an unsafe context because it has pointers in its signature + // c2[0] = c2[0]; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c2[0]").WithArguments("C2.this[int].get").WithLocation(4, 9)); + + CompileAndVerify(""" + var c1 = new C1(); + c1[0] = c1[0]; + var c2 = new C2(); + unsafe { c2[0] = c2[0]; } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: ["C2.this[]", "C2.get_Item", "C2.set_Item"], + expectedSafeSymbols: ["C1", "C2", "C1.this[]", "C1.get_Item", "C1.set_Item"], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (4,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c2[0] = c2[0]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c2[0]").WithLocation(4, 1), + // (4,9): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c2[0] = c2[0]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c2[0]").WithLocation(4, 9), + // (4,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c2[0] = c2[0]; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c2[0] = c2[0]").WithLocation(4, 1)); + } + + [Theory, CombinatorialData] + public void CompatMode_Event(bool useCompilationReference) + { + var lib = CreateCompilation(""" + #pragma warning disable CS0067 // unused event + public class C + { + public unsafe event System.Action E1; + public unsafe event System.Action E2; + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var c = new C(); + c.E1 += null; + c.E2 += null; + unsafe { c.E2 += null; } + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,6): error CS9363: 'C.E2.add' must be used in an unsafe context because it has pointers in its signature + // c.E2 += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "+=").WithArguments("C.E2.add").WithLocation(3, 6)); + + CompileAndVerify(""" + var c = new C(); + c.E1 += null; + unsafe { c.E2 += null; } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: ["C.E2", "C.add_E2", "C.remove_E2"], + expectedSafeSymbols: ["C", "C.E1", "C.add_E1", "C.remove_E1"], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (3,1): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // c.E2 += null; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "c.E2").WithLocation(3, 1)); + } + + [Theory, CombinatorialData] + public void CompatMode_Constructor(bool useCompilationReference) + { + var lib = CreateCompilation(""" + public class C + { + public unsafe C() { } + public unsafe C(int* p) { } + } + """, + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + _ = new C(); + _ = new C(null); + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,5): error CS9363: 'C.C(int*)' must be used in an unsafe context because it has pointers in its signature + // _ = new C(null); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "new C(null)").WithArguments("C.C(int*)").WithLocation(2, 5)); + + CompileAndVerify(""" + _ = new C(); + unsafe { _ = new C(null); } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: [Overload("C..ctor", parameterCount: 1)], + expectedSafeSymbols: ["C", Overload("C..ctor", parameterCount: 0)], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyDiagnostics( + // (2,5): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // _ = new C(null); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "new C(null)").WithLocation(2, 5), + // (2,11): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // _ = new C(null); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "null").WithLocation(2, 11)); + } + + [Theory, CombinatorialData] + public void CompatMode_Operator(bool useCompilationReference) + { + var lib = CreateCompilation( + [ + """ + public class C + { + public unsafe void operator +=(int i) { } + public unsafe void operator -=(int* p) { } + } + """, + CompilerFeatureRequiredAttribute, + ], + options: TestOptions.UnsafeReleaseDll, + assemblyName: "lib") + .VerifyDiagnostics(); + var libRef = AsReference(lib, useCompilationReference); + + var source = """ + var c = new C(); + c += 0; + c -= null; + """; + + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,1): error CS9363: 'C.operator -=(int*)' must be used in an unsafe context because it has pointers in its signature + // c -= null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c -= null").WithArguments("C.operator -=(int*)").WithLocation(3, 1)); + + CompileAndVerify(""" + var c = new C(); + c += 0; + unsafe { c -= null; } + """, + [libRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m.ReferencedAssemblySymbols.Single(a => a.Name == "lib").Modules.Single(), + expectedUnsafeSymbols: ["C.op_SubtractionAssignment"], + expectedSafeSymbols: ["C", "C.op_AdditionAssignment"], + expectedUnsafeMode: CallerUnsafeMode.Implicit)) + .VerifyDiagnostics(); + + // https://github.com/dotnet/roslyn/issues/81967: operator invocations involving pointers are allowed outside unsafe context + CreateCompilation(source, + [libRef], + options: TestOptions.UnsafeReleaseExe) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void Extern_Method() + { + var libSource = """ + #pragma warning disable CS0626 // extern without attributes + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + public class C + { + public void M1() { } + public extern void M2(); + [DllImport("test")] public static extern void M3(); + [MethodImpl(MethodImplOptions.InternalCall)] public extern void M4(); + } + """; + + var callerSource = """ + var c = new C(); + c.M1(); + c.M2(); + C.M3(); + c.M4(); + """; + + object[] unsafeSymbols = ["C.M2", "C.M3", "C.M4"]; + + var commonDiagnostics = new[] + { + // (3,1): error CS9362: 'C.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M2()").WithArguments("C.M2()").WithLocation(3, 1), + // (4,1): error CS9362: 'C.M3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C.M3(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.M3()").WithArguments("C.M3()").WithLocation(4, 1), + // (5,1): error CS9362: 'C.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M4(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M4()").WithArguments("C.M4()").WithLocation(5, 1), + }; + + CompileAndVerifyUnsafe( + libSource, + callerSource, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["C", "C.M1"], + expectedNoAttributeInSource: unsafeSymbols, + expectedNoAttributeUnderLegacyRules: unsafeSymbols, + expectedDiagnostics: commonDiagnostics); + + CreateCompilation([libSource, callerSource], + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + [ + .. commonDiagnostics, + // (8,24): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern void M2(); + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "M2").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(8, 24), + // (9,51): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // [DllImport("test")] public static extern void M3(); + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "M3").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(9, 51), + // (10,69): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // [MethodImpl(MethodImplOptions.InternalCall)] public extern void M4(); + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "M4").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(10, 69), + ]); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (8,24): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern void M2(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "M2").WithArguments("updated memory safety rules").WithLocation(8, 24), + // (9,51): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [DllImport("test")] public static extern void M3(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "M3").WithArguments("updated memory safety rules").WithLocation(9, 51), + // (10,69): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // [MethodImpl(MethodImplOptions.InternalCall)] public extern void M4(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "M4").WithArguments("updated memory safety rules").WithLocation(10, 69)); + + CreateCompilation(libSource, + parseOptions: TestOptions.RegularNext) + .VerifyEmitDiagnostics(); + + CreateCompilation(libSource, + parseOptions: TestOptions.Regular14) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void Extern_Method_WithPointers() + { + static string getLibSource(string modifiers) => $$""" + #pragma warning disable CS0626 // extern without attributes + public class C + { + public {{modifiers}} int* M(); + } + """; + + var callerSource = """ + var c = new C(); + c.M(); + """; + + var libUpdated = CreateCompilation( + [getLibSource("extern"), RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libUpdatedRef = AsReference(libUpdated, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libUpdatedRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M()").WithArguments("C.M()").WithLocation(2, 1)) + .GetReferencedAssemblySymbol(libUpdatedRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: ["C.M"]); + } + + CreateCompilation(getLibSource("extern")).VerifyDiagnostics( + // (4,19): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public extern int* M(); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(4, 19)); + + // When compiling the lib under legacy rules, extern members are not unsafe, but members with pointers are. + var libLegacy = CreateCompilation( + getLibSource("unsafe extern"), + options: TestOptions.UnsafeReleaseDll) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libLegacyRef = AsReference(libLegacy, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libLegacyRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9363: 'C.M()' must be used in an unsafe context because it has pointers in its signature + // c.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c.M()").WithArguments("C.M()").WithLocation(2, 1)) + .GetReferencedAssemblySymbol(libLegacyRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedUnsafeMode: CallerUnsafeMode.Implicit); + } + } + + [Fact] + public void Extern_Method_Explicit() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public extern void M(); + } + """, + caller: """ + var c = new C(); + c.M(); + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M()").WithArguments("C.M()").WithLocation(2, 1), + ]); + } + + [Fact] + public void Extern_Method_Override() + { + object[] unsafeSymbols = ["B.M1", "B.M2", "B.M3"]; + + CompileAndVerifyUnsafe( + lib: """ + #pragma warning disable CS0626 // extern without attributes + public class B + { + public extern virtual void M1(); + public extern virtual void M2(); + public extern virtual void M3(); + public virtual void M4() { } + public virtual void M5() { } + public virtual void M6() { } + } + """, + caller: """ + #pragma warning disable CS0626 // extern without attributes + var d1 = new D1(); d1.M1(); d1.M2(); d1.M3(); d1.M4(); d1.M5(); d1.M6(); + var d2 = new D2(); d2.M1(); d2.M2(); d2.M3(); d2.M4(); d2.M5(); d2.M6(); + var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + + class C : B + { + public extern override void M1(); + public extern new virtual void M2(); + public extern override void M4(); + public extern new virtual void M5(); + } + + class D1 : C + { + public override void M1() { } + public override void M2() { } + public override void M3() { } + public override void M4() { } + public override void M5() { } + public override void M6() { } + public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + } + + class D2 : C + { + public unsafe override void M1() { } + public unsafe override void M2() { } + public unsafe override void M3() { } + public unsafe override void M4() { } + public unsafe override void M5() { } + public unsafe override void M6() { } + } + + class D3 : C + { + public extern override void M1(); + public extern override void M2(); + public extern override void M3(); + public extern override void M4(); + public extern override void M5(); + public extern override void M6(); + } + + class D4 : C; + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["B", "B.M4", "B.M5", "B.M6"], + expectedNoAttributeInSource: unsafeSymbols, + expectedNoAttributeUnderLegacyRules: unsafeSymbols, + expectedDiagnostics: + [ + // (4,20): error CS9362: 'D3.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M1()").WithArguments("D3.M1()").WithLocation(4, 20), + // (4,29): error CS9362: 'D3.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M2()").WithArguments("D3.M2()").WithLocation(4, 29), + // (4,38): error CS9362: 'D3.M3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M3()").WithArguments("D3.M3()").WithLocation(4, 38), + // (4,47): error CS9362: 'D3.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M4()").WithArguments("D3.M4()").WithLocation(4, 47), + // (4,56): error CS9362: 'D3.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M5()").WithArguments("D3.M5()").WithLocation(4, 56), + // (4,65): error CS9362: 'D3.M6()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M6()").WithArguments("D3.M6()").WithLocation(4, 65), + // (5,20): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d4.M1()").WithArguments("C.M1()").WithLocation(5, 20), + // (5,29): error CS9362: 'C.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d4.M2()").WithArguments("C.M2()").WithLocation(5, 29), + // (5,38): error CS9362: 'B.M3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d4.M3()").WithArguments("B.M3()").WithLocation(5, 38), + // (5,47): error CS9362: 'C.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d4.M4()").WithArguments("C.M4()").WithLocation(5, 47), + // (5,56): error CS9362: 'C.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d4.M5()").WithArguments("C.M5()").WithLocation(5, 56), + // (6,11): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M1()").WithArguments("C.M1()").WithLocation(6, 11), + // (6,19): error CS9362: 'C.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M2()").WithArguments("C.M2()").WithLocation(6, 19), + // (6,27): error CS9362: 'B.M3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M3()").WithArguments("B.M3()").WithLocation(6, 27), + // (6,35): error CS9362: 'C.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M4()").WithArguments("C.M4()").WithLocation(6, 35), + // (6,43): error CS9362: 'C.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M5()").WithArguments("C.M5()").WithLocation(6, 43), + // (12,33): error CS9364: Unsafe member 'C.M4()' cannot override safe member 'B.M4()' + // public extern override void M4(); + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M4").WithArguments("C.M4()", "B.M4()").WithLocation(12, 33), + // (24,31): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M1()").WithArguments("C.M1()").WithLocation(24, 31), + // (24,42): error CS9362: 'C.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M2()").WithArguments("C.M2()").WithLocation(24, 42), + // (24,53): error CS9362: 'B.M3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M3()").WithArguments("B.M3()").WithLocation(24, 53), + // (24,64): error CS9362: 'C.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M4()").WithArguments("C.M4()").WithLocation(24, 64), + // (24,75): error CS9362: 'C.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M5()").WithArguments("C.M5()").WithLocation(24, 75), + // (42,33): error CS9364: Unsafe member 'D3.M4()' cannot override safe member 'B.M4()' + // public extern override void M4(); + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M4").WithArguments("D3.M4()", "B.M4()").WithLocation(42, 33), + // (44,33): error CS9364: Unsafe member 'D3.M6()' cannot override safe member 'B.M6()' + // public extern override void M6(); + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M6").WithArguments("D3.M6()", "B.M6()").WithLocation(44, 33), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (4,20): error CS9362: 'D3.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M1()").WithArguments("D3.M1()").WithLocation(4, 20), + // (4,29): error CS9362: 'D3.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M2()").WithArguments("D3.M2()").WithLocation(4, 29), + // (4,38): error CS9362: 'D3.M3()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M3()").WithArguments("D3.M3()").WithLocation(4, 38), + // (4,47): error CS9362: 'D3.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M4()").WithArguments("D3.M4()").WithLocation(4, 47), + // (4,56): error CS9362: 'D3.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M5()").WithArguments("D3.M5()").WithLocation(4, 56), + // (4,65): error CS9362: 'D3.M6()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d3 = new D3(); d3.M1(); d3.M2(); d3.M3(); d3.M4(); d3.M5(); d3.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d3.M6()").WithArguments("D3.M6()").WithLocation(4, 65), + // (5,20): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d4.M1()").WithArguments("C.M1()").WithLocation(5, 20), + // (5,29): error CS9362: 'C.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d4.M2()").WithArguments("C.M2()").WithLocation(5, 29), + // (5,47): error CS9362: 'C.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d4.M4()").WithArguments("C.M4()").WithLocation(5, 47), + // (5,56): error CS9362: 'C.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // var d4 = new D4(); d4.M1(); d4.M2(); d4.M3(); d4.M4(); d4.M5(); d4.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "d4.M5()").WithArguments("C.M5()").WithLocation(5, 56), + // (6,11): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M1()").WithArguments("C.M1()").WithLocation(6, 11), + // (6,19): error CS9362: 'C.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M2()").WithArguments("C.M2()").WithLocation(6, 19), + // (6,35): error CS9362: 'C.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M4()").WithArguments("C.M4()").WithLocation(6, 35), + // (6,43): error CS9362: 'C.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C c = d1; c.M1(); c.M2(); c.M3(); c.M4(); c.M5(); c.M6(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M5()").WithArguments("C.M5()").WithLocation(6, 43), + // (10,33): error CS9364: Unsafe member 'C.M1()' cannot override safe member 'B.M1()' + // public extern override void M1(); + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M1").WithArguments("C.M1()", "B.M1()").WithLocation(10, 33), + // (12,33): error CS9364: Unsafe member 'C.M4()' cannot override safe member 'B.M4()' + // public extern override void M4(); + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M4").WithArguments("C.M4()", "B.M4()").WithLocation(12, 33), + // (24,31): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M1()").WithArguments("C.M1()").WithLocation(24, 31), + // (24,42): error CS9362: 'C.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M2()").WithArguments("C.M2()").WithLocation(24, 42), + // (24,64): error CS9362: 'C.M4()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M4()").WithArguments("C.M4()").WithLocation(24, 64), + // (24,75): error CS9362: 'C.M5()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // public void BaseCalls() { base.M1(); base.M2(); base.M3(); base.M4(); base.M5(); base.M6(); } + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "base.M5()").WithArguments("C.M5()").WithLocation(24, 75), + // (39,33): error CS9364: Unsafe member 'D3.M1()' cannot override safe member 'B.M1()' + // public extern override void M1(); + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M1").WithArguments("D3.M1()", "B.M1()").WithLocation(39, 33), + // (41,33): error CS9364: Unsafe member 'D3.M3()' cannot override safe member 'B.M3()' + // public extern override void M3(); + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M3").WithArguments("D3.M3()", "B.M3()").WithLocation(41, 33), + // (42,33): error CS9364: Unsafe member 'D3.M4()' cannot override safe member 'B.M4()' + // public extern override void M4(); + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M4").WithArguments("D3.M4()", "B.M4()").WithLocation(42, 33), + // (44,33): error CS9364: Unsafe member 'D3.M6()' cannot override safe member 'B.M6()' + // public extern override void M6(); + Diagnostic(ErrorCode.ERR_CallerUnsafeOverridingSafe, "M6").WithArguments("D3.M6()", "B.M6()").WithLocation(44, 33), + ]); + } + + [Fact] + public void Extern_Method_Implementation() + { + CompileAndVerifyUnsafe( + lib: """ + #pragma warning disable CS0626 // extern without attributes + public interface I + { + extern void M1(); + void M2(); + } + """, + caller: """ + #pragma warning disable CS0626 // extern without attributes + I i = new C1(); + i.M1(); + i.M2(); + + public class C1 : I + { + public void M1() { } + public void M2() { } + } + + public class C2 : I + { + void I.M1() { } + void I.M2() { } + } + + public class C3 : I + { + public unsafe void M1() { } + public unsafe void M2() { } + } + + public class C4 : I + { + unsafe void I.M1() { } + unsafe void I.M2() { } + } + + public class C5 : I + { + public extern void M1(); + public extern void M2(); + } + + public class C6 : I + { + extern void I.M1(); + extern void I.M2(); + } + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + targetFramework: TargetFramework.Net100, + verify: Verification.Skipped, + expectedUnsafeSymbols: ["I.M1"], + expectedSafeSymbols: ["I", "I.M2"], + expectedNoAttributeInSource: ["I.M1"], + expectedNoAttributeUnderLegacyRules: ["I.M1"], + expectedDiagnostics: + [ + // (3,1): error CS9362: 'I.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // i.M1(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "i.M1()").WithArguments("I.M1()").WithLocation(3, 1), + // (33,24): error CS9365: Unsafe member 'C5.M2()' cannot implicitly implement safe member 'I.M2()' + // public extern void M2(); + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C5.M2()", "I.M2()").WithLocation(33, 24), + // (39,19): error CS9366: Unsafe member 'C6.I.M2()' cannot implement safe member 'I.M2()' + // extern void I.M2(); + Diagnostic(ErrorCode.ERR_CallerUnsafeExplicitlyImplementingSafe, "M2").WithArguments("C6.I.M2()", "I.M2()").WithLocation(39, 19), + ], + expectedDiagnosticsWhenReferencingLegacyLib: + [ + // (32,24): error CS9365: Unsafe member 'C5.M1()' cannot implicitly implement safe member 'I.M1()' + // public extern void M1(); + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M1").WithArguments("C5.M1()", "I.M1()").WithLocation(32, 24), + // (33,24): error CS9365: Unsafe member 'C5.M2()' cannot implicitly implement safe member 'I.M2()' + // public extern void M2(); + Diagnostic(ErrorCode.ERR_CallerUnsafeImplicitlyImplementingSafe, "M2").WithArguments("C5.M2()", "I.M2()").WithLocation(33, 24), + // (38,19): error CS9366: Unsafe member 'C6.I.M1()' cannot implement safe member 'I.M1()' + // extern void I.M1(); + Diagnostic(ErrorCode.ERR_CallerUnsafeExplicitlyImplementingSafe, "M1").WithArguments("C6.I.M1()", "I.M1()").WithLocation(38, 19), + // (39,19): error CS9366: Unsafe member 'C6.I.M2()' cannot implement safe member 'I.M2()' + // extern void I.M2(); + Diagnostic(ErrorCode.ERR_CallerUnsafeExplicitlyImplementingSafe, "M2").WithArguments("C6.I.M2()", "I.M2()").WithLocation(39, 19), + ]); + } + + [Fact] + public void Extern_LocalFunction() + { + var libSource = """ + #pragma warning disable CS0626 // extern without attributes + #pragma warning disable CS8321 // unused local function + public class C + { + public void M() + { + static extern void F(); + } + } + """; + + var callerSource = """ + new C().M(); + """; + + object[] unsafeSymbols = ["C.g__F|0_0"]; + + CompileAndVerifyUnsafe( + libSource, + callerSource, + additionalSources: [RequiresUnsafeAttributeDefinition], + optionsDll: TestOptions.UnsafeReleaseDll.WithMetadataImportOptions(MetadataImportOptions.All), + verify: Verification.Skipped, + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: unsafeSymbols, + expectedNoAttributeUnderLegacyRules: unsafeSymbols, + skipSymbolsInSource: unsafeSymbols, + expectedDiagnostics: []); + + CreateCompilation([libSource, callerSource], + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (7,28): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // static extern void F(); + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "F").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(7, 28)); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (7,28): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // static extern void F(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "F").WithArguments("updated memory safety rules").WithLocation(7, 28)); + + CreateCompilation(libSource, + parseOptions: TestOptions.RegularNext) + .VerifyEmitDiagnostics(); + + CreateCompilation(libSource, + parseOptions: TestOptions.Regular14) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void Extern_Property() + { + var libSource = """ + #pragma warning disable CS0626 // extern without attributes + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + + public class C + { + public int P1 { set { } } + public extern int P2 { set; } + public static extern int P3 { [DllImport("test")] set; } + public extern int P4 { [MethodImpl(MethodImplOptions.InternalCall)] set; } + } + """; + + var callerSource = """ + var c = new C(); + c.P1 = 0; + c.P2 = 0; + C.P3 = 0; + c.P4 = 0; + """; + + object[] unsafeSymbols = ["C.P2", "C.set_P2", "C.P3", "C.set_P3", "C.P4", "C.set_P4"]; + + var commonDiagnostics = new[] + { + // (3,1): error CS9362: 'C.P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = 0; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C.P2.set").WithLocation(3, 1), + // (4,1): error CS9362: 'C.P3.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C.P3 = 0; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "C.P3").WithArguments("C.P3.set").WithLocation(4, 1), + // (5,1): error CS9362: 'C.P4.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P4 = 0; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P4").WithArguments("C.P4.set").WithLocation(5, 1), + }; + + CompileAndVerifyUnsafe( + libSource, + callerSource, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["C", "C.P1", "C.set_P1"], + expectedNoAttributeInSource: unsafeSymbols, + expectedNoAttributeUnderLegacyRules: unsafeSymbols, + expectedDiagnostics: commonDiagnostics); + + CreateCompilation([libSource, callerSource], + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics([ + .. commonDiagnostics, + // (8,23): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern int P2 { set; } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "P2").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(8, 23), + // (8,28): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern int P2 { set; } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "set").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(8, 28), + // (9,30): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public static extern int P3 { [DllImport("test")] set; } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "P3").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(9, 30), + // (9,55): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public static extern int P3 { [DllImport("test")] set; } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "set").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(9, 55), + // (10,23): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern int P4 { [MethodImpl(MethodImplOptions.InternalCall)] set; } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "P4").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(10, 23), + // (10,73): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern int P4 { [MethodImpl(MethodImplOptions.InternalCall)] set; } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "set").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(10, 73), + ]); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.RegularNext, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.Regular14, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (8,23): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern int P2 { set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "P2").WithArguments("updated memory safety rules").WithLocation(8, 23), + // (8,28): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern int P2 { set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "set").WithArguments("updated memory safety rules").WithLocation(8, 28), + // (9,30): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public static extern int P3 { [DllImport("test")] set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "P3").WithArguments("updated memory safety rules").WithLocation(9, 30), + // (9,55): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public static extern int P3 { [DllImport("test")] set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "set").WithArguments("updated memory safety rules").WithLocation(9, 55), + // (10,23): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern int P4 { [MethodImpl(MethodImplOptions.InternalCall)] set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "P4").WithArguments("updated memory safety rules").WithLocation(10, 23), + // (10,73): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern int P4 { [MethodImpl(MethodImplOptions.InternalCall)] set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "set").WithArguments("updated memory safety rules").WithLocation(10, 73)); + + CreateCompilation(libSource, + parseOptions: TestOptions.RegularNext) + .VerifyEmitDiagnostics(); + + CreateCompilation(libSource, + parseOptions: TestOptions.Regular14) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void Extern_Property_WithPointers() + { + static string getLibSource(string modifiers) => $$""" + #pragma warning disable CS0626 // extern without attributes + public class C + { + public {{modifiers}} int* P { set; } + } + """; + + var callerSource = """ + var c = new C(); + c.P = null; + """; + + var libUpdated = CreateCompilation( + [getLibSource("extern"), RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libUpdatedRef = AsReference(libUpdated, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libUpdatedRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9362: 'C.P.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P = null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P").WithArguments("C.P.set").WithLocation(2, 1)) + .GetReferencedAssemblySymbol(libUpdatedRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C.P", "C.set_P"], + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: ["C.P", "C.set_P"]); + } + + CreateCompilation(getLibSource("extern")).VerifyDiagnostics( + // (4,19): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public extern int* P { set; } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(4, 19)); + + // When compiling the lib under legacy rules, extern members are not unsafe, but members with pointers are. + var libLegacy = CreateCompilation( + getLibSource("unsafe extern"), + options: TestOptions.UnsafeReleaseDll) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libLegacyRef = AsReference(libLegacy, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libLegacyRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9363: 'C.P.set' must be used in an unsafe context because it has pointers in its signature + // c.P = null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c.P").WithArguments("C.P.set").WithLocation(2, 1)) + .GetReferencedAssemblySymbol(libLegacyRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C.P", "C.set_P"], + expectedSafeSymbols: ["C"], + expectedUnsafeMode: CallerUnsafeMode.Implicit); + } + } + + [Fact] + public void Extern_Property_Explicit() + { + CompileAndVerifyUnsafe( + lib: """ + #pragma warning disable CS0626 // extern without attributes + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public extern int P1 { set; } + public extern int P2 { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set; } + } + """, + caller: """ + var c = new C(); + c.P1 = 0; + c.P2 = 0; + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["C.P1", "C.set_P1", "C.P2", "C.set_P2"], + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: ["C.P2"], + expectedNoAttributeUnderLegacyRules: ["C.P2"], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'C.P1.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P1 = 0; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P1").WithArguments("C.P1.set").WithLocation(2, 1), + // (3,1): error CS9362: 'C.P2.set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.P2 = 0; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.P2").WithArguments("C.P2.set").WithLocation(3, 1), + ]); + } + + [Fact] + public void Extern_Indexer() + { + var libSource = """ + #pragma warning disable CS0626 // extern without attributes + public class C + { + public extern int this[int i] { get; set; } + } + """; + + var callerSource = """ + var c = new C(); + c[0] = c[0] + 123; + """; + + object[] unsafeSymbols = ["C.this[]", "C.get_Item", "C.set_Item"]; + + var commonDiagnostics = new[] + { + // (2,1): error CS9362: 'C.this[int].set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c[0] = c[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c[0]").WithArguments("C.this[int].set").WithLocation(2, 1), + // (2,8): error CS9362: 'C.this[int].get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c[0] = c[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c[0]").WithArguments("C.this[int].get").WithLocation(2, 8), + }; + + CompileAndVerifyUnsafe( + libSource, + callerSource, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: unsafeSymbols, + expectedNoAttributeUnderLegacyRules: unsafeSymbols, + expectedDiagnostics: commonDiagnostics); + + CreateCompilation([libSource, callerSource], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + [ + .. commonDiagnostics, + // (4,23): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern int this[int i] { get; set; } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "this").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(4, 23), + // (4,37): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern int this[int i] { get; set; } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "get").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(4, 37), + // (4,42): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern int this[int i] { get; set; } + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "set").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(4, 42), + ]); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (4,23): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern int this[int i] { get; set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "this").WithArguments("updated memory safety rules").WithLocation(4, 23), + // (4,37): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern int this[int i] { get; set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "get").WithArguments("updated memory safety rules").WithLocation(4, 37), + // (4,42): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern int this[int i] { get; set; } + Diagnostic(ErrorCode.ERR_FeatureInPreview, "set").WithArguments("updated memory safety rules").WithLocation(4, 42)); + + CreateCompilation(libSource, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseDll) + .VerifyEmitDiagnostics(); + + CreateCompilation(libSource, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseDll) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void Extern_Indexer_WithPointers() + { + static string getLibSource(string modifiers) => $$""" + #pragma warning disable CS0626 // extern without attributes + public class C + { + public {{modifiers}} int* this[int i] { get; set; } + } + """; + + var callerSource = """ + var c = new C(); + c[0] = c[0] + 123; + """; + + var libUpdated = CreateCompilation( + [getLibSource("extern"), RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libUpdatedRef = AsReference(libUpdated, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libUpdatedRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9362: 'C.this[int].set' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c[0] = c[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c[0]").WithArguments("C.this[int].set").WithLocation(2, 1), + // (2,8): error CS9362: 'C.this[int].get' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c[0] = c[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c[0]").WithArguments("C.this[int].get").WithLocation(2, 8)) + .GetReferencedAssemblySymbol(libUpdatedRef); + + object[] unsafeSymbols = ["C.this[]", "C.get_Item", "C.set_Item"]; + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: unsafeSymbols); + } + + CreateCompilation(getLibSource("extern")).VerifyDiagnostics( + // (4,19): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public extern int* P { set; } + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(4, 19)); + + // When compiling the lib under legacy rules, extern members are not unsafe, but members with pointers are. + var libLegacy = CreateCompilation( + getLibSource("unsafe extern"), + options: TestOptions.UnsafeReleaseDll) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libLegacyRef = AsReference(libLegacy, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libLegacyRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9363: 'C.this[int].set' must be used in an unsafe context because it has pointers in its signature + // c[0] = c[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c[0]").WithArguments("C.this[int].set").WithLocation(2, 1), + // (2,8): error CS9363: 'C.this[int].get' must be used in an unsafe context because it has pointers in its signature + // c[0] = c[0] + 123; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c[0]").WithArguments("C.this[int].get").WithLocation(2, 8)) + .GetReferencedAssemblySymbol(libLegacyRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C.this[]", "C.get_Item", "C.set_Item"], + expectedSafeSymbols: ["C"], + expectedUnsafeMode: CallerUnsafeMode.Implicit); + } + } + + [Fact] + public void Extern_Indexer_Explicit() + { + CompileAndVerifyUnsafe( + lib: """ + #pragma warning disable CS0626 // extern without attributes + public class C1 + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public extern int this[int i] { set; } + } + public class C2 + { + public extern int this[int i] { [System.Diagnostics.CodeAnalysis.RequiresUnsafe] set; } + } + """, + caller: """ + new C1()[0] = 0; + new C2()[0] = 0; + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["C1.this[]", "C1.set_Item", "C2.this[]", "C2.set_Item"], + expectedSafeSymbols: ["C1", "C2"], + expectedNoAttributeInSource: ["C2.this[]"], + expectedNoAttributeUnderLegacyRules: ["C2.this[]"], + expectedDiagnostics: + [ + // (1,1): error CS9362: 'C1.this[int].set' must be used in an unsafe context because it is marked as 'unsafe' or 'extern' + // new C1()[0] = 0; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C1()[0]").WithArguments("C1.this[int].set").WithLocation(1, 1), + // (2,1): error CS9362: 'C2.this[int].set' must be used in an unsafe context because it is marked as 'unsafe' or 'extern' + // new C2()[0] = 0; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C2()[0]").WithArguments("C2.this[int].set").WithLocation(2, 1), + ]); + } + + [Fact] + public void Extern_Event() + { + var libSource = """ + #pragma warning disable CS0067 // unused event + public class C + { + [method: System.Runtime.InteropServices.DllImport("test")] + public static extern event System.Action E; + } + """; + + var callerSource = """ + C.E += null; + """; + + object[] unsafeSymbols = ["C.E", "C.add_E", "C.remove_E"]; + + var commonDiagnostics = new[] + { + // (1,5): error CS9362: 'C.E.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C.E += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("C.E.add").WithLocation(1, 5), + }; + + CompileAndVerifyUnsafe( + libSource, + callerSource, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: unsafeSymbols, + expectedNoAttributeUnderLegacyRules: unsafeSymbols, + expectedDiagnostics: commonDiagnostics); + + CreateCompilation([libSource, callerSource], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics([ + .. commonDiagnostics, + // (5,46): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public static extern event System.Action E; + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "E").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(5, 46), + // (5,46): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public static extern event System.Action E; + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "E").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(5, 46), + // (5,46): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public static extern event System.Action E; + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "E").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(5, 46), + ]); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (5,46): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public static extern event System.Action E; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "E").WithArguments("updated memory safety rules").WithLocation(5, 46), + // (5,46): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public static extern event System.Action E; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "E").WithArguments("updated memory safety rules").WithLocation(5, 46), + // (5,46): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public static extern event System.Action E; + Diagnostic(ErrorCode.ERR_FeatureInPreview, "E").WithArguments("updated memory safety rules").WithLocation(5, 46)); + + CreateCompilation(libSource, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseDll) + .VerifyEmitDiagnostics(); + + CreateCompilation(libSource, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseDll) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void Extern_Event_WithPointers() + { + var libSource = """ + #pragma warning disable CS0067 // unused event + public class C + { + [method: System.Runtime.InteropServices.DllImport("test")] + public static extern event System.Action E; + } + """; + + var callerSource = """ + C.E += null; + """; + + var libUpdated = CreateCompilation( + [libSource, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libUpdatedRef = AsReference(libUpdated, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libUpdatedRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,5): error CS9362: 'C.E.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C.E += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("C.E.add").WithLocation(1, 5)) + .GetReferencedAssemblySymbol(libUpdatedRef); + + object[] unsafeSymbols = ["C.E", "C.add_E", "C.remove_E"]; + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: unsafeSymbols); + } + + // When compiling the lib under legacy rules, extern members are not unsafe, but members with pointers are. + // https://github.com/dotnet/roslyn/issues/81944: There is no error for the pointer even though `unsafe` is missing. + var libLegacy = CreateCompilation( + libSource) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libLegacyRef = AsReference(libLegacy, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libLegacyRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,5): error CS9363: 'C.E.add' must be used in an unsafe context because it has pointers in its signature + // C.E += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "+=").WithArguments("C.E.add").WithLocation(1, 5)) + .GetReferencedAssemblySymbol(libLegacyRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C.E", "C.add_E", "C.remove_E"], + expectedSafeSymbols: ["C"], + expectedUnsafeMode: CallerUnsafeMode.Implicit); + } + } + + [Fact] + public void Extern_Event_Explicit() + { + CompileAndVerifyUnsafe( + lib: """ + #pragma warning disable CS0626 // extern without attributes + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public static extern event System.Action E; + } + """, + caller: """ + C.E += null; + C.E -= null; + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["C.E", "C.add_E", "C.remove_E"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,5): error CS9362: 'C.E.add' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C.E += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "+=").WithArguments("C.E.add").WithLocation(1, 5), + // (2,5): error CS9362: 'C.E.remove' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // C.E -= null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "-=").WithArguments("C.E.remove").WithLocation(2, 5), + ]); + } + + [Fact] + public void Extern_Constructor() + { + var libSource = """ + #pragma warning disable CS0824 // extern constructor + public class C + { + public extern C(); + } + """; + + var callerSource = """ + _ = new C(); + """; + + object[] unsafeSymbols = ["C..ctor"]; + + var commonDiagnostics = new[] + { + // (1,5): error CS9362: 'C.C()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C()").WithArguments("C.C()").WithLocation(1, 5), + }; + + CompileAndVerifyUnsafe( + libSource, + callerSource, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: unsafeSymbols, + expectedNoAttributeUnderLegacyRules: unsafeSymbols, + expectedDiagnostics: commonDiagnostics); + + CreateCompilation([libSource, callerSource], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics([ + .. commonDiagnostics, + // (4,19): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern C(); + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "C").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(4, 19), + ]); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(); + + CreateCompilation([libSource, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (4,19): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern C(); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "C").WithArguments("updated memory safety rules").WithLocation(4, 19)); + + CreateCompilation(libSource, + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseDll) + .VerifyEmitDiagnostics(); + + CreateCompilation(libSource, + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseDll) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void Extern_Constructor_WithPointers() + { + static string getLibSource(string modifiers) => $$""" + #pragma warning disable CS0824 // extern constructor + public class C + { + public {{modifiers}} C(int* p); + } + """; + + var callerSource = """ + _ = new C(null); + """; + + var libUpdated = CreateCompilation( + [getLibSource("extern"), RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libUpdatedRef = AsReference(libUpdated, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libUpdatedRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,5): error CS9362: 'C.C(int*)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C(null); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C(null)").WithArguments("C.C(int*)").WithLocation(1, 5)) + .GetReferencedAssemblySymbol(libUpdatedRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: ["C..ctor"]); + } + + CreateCompilation(getLibSource("extern")).VerifyDiagnostics( + // (4,21): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public extern C(int* p); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(4, 21)); + + // When compiling the lib under legacy rules, extern members are not unsafe, but members with pointers are. + var libLegacy = CreateCompilation( + getLibSource("unsafe extern"), + options: TestOptions.UnsafeReleaseDll) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libLegacyRef = AsReference(libLegacy, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libLegacyRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,5): error CS9363: 'C.C(int*)' must be used in an unsafe context because it has pointers in its signature + // _ = new C(null); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "new C(null)").WithArguments("C.C(int*)").WithLocation(1, 5)) + .GetReferencedAssemblySymbol(libLegacyRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C"], + expectedUnsafeMode: CallerUnsafeMode.Implicit); + } + } + + [Fact] + public void Extern_Constructor_Explicit() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public extern C(); + } + """, + caller: """ + _ = new C(); + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["C..ctor"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (1,5): error CS9362: 'C.C()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // _ = new C(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C()").WithArguments("C.C()").WithLocation(1, 5), + ]); + } + + [Fact] + public void Extern_Operator() + { + var libSource = """ + #pragma warning disable CS0626 // extern without attributes + public class C + { + public extern void operator +=(C c); + } + """; + + var callerSource = """ + var c = new C(); + c += c; + """; + + object[] unsafeSymbols = ["C.op_AdditionAssignment"]; + + var commonDiagnostics = new[] + { + // (2,1): error CS9362: 'C.operator +=(C)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c += c; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c += c").WithArguments("C.operator +=(C)").WithLocation(2, 1), + }; + + CompileAndVerifyUnsafe( + libSource, + callerSource, + additionalSources: [CompilerFeatureRequiredAttribute, RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: unsafeSymbols, + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: unsafeSymbols, + expectedNoAttributeUnderLegacyRules: unsafeSymbols, + expectedDiagnostics: commonDiagnostics); + + CreateCompilation([libSource, callerSource, CompilerFeatureRequiredAttribute], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics([ + .. commonDiagnostics, + // (4,33): error CS0656: Missing compiler required member 'System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute..ctor' + // public extern void operator +=(C c); + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "+=").WithArguments("System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", ".ctor").WithLocation(4, 33), + ]); + + CreateCompilation([libSource, CompilerFeatureRequiredAttribute, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(); + + CreateCompilation([libSource, CompilerFeatureRequiredAttribute, RequiresUnsafeAttributeDefinition], + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // error CS8630: Invalid 'MemorySafetyRules' value: '2' for C# 14.0. Please use language version 'preview' or greater. + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("MemorySafetyRules", "2", "14.0", "preview").WithLocation(1, 1), + // (4,33): error CS8652: The feature 'updated memory safety rules' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version. + // public extern void operator +=(C c); + Diagnostic(ErrorCode.ERR_FeatureInPreview, "+=").WithArguments("updated memory safety rules").WithLocation(4, 33)); + + CreateCompilation([libSource, CompilerFeatureRequiredAttribute], + parseOptions: TestOptions.RegularNext, + options: TestOptions.UnsafeReleaseDll) + .VerifyEmitDiagnostics(); + + CreateCompilation([libSource, CompilerFeatureRequiredAttribute], + parseOptions: TestOptions.Regular14, + options: TestOptions.UnsafeReleaseDll) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void Extern_Operator_WithPointers() + { + static string getLibSource(string modifiers) => $$""" + #pragma warning disable CS0626 // extern without attributes + public class C + { + public {{modifiers}} void operator +=(int* p); + } + """; + + var callerSource = """ + var c = new C(); + c += null; + """; + + var libUpdated = CreateCompilation( + [getLibSource("extern"), CompilerFeatureRequiredAttribute, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libUpdatedRef = AsReference(libUpdated, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libUpdatedRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9362: 'C.operator +=(int*)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c += null").WithArguments("C.operator +=(int*)").WithLocation(2, 1)) + .GetReferencedAssemblySymbol(libUpdatedRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C.op_AdditionAssignment"], + expectedSafeSymbols: ["C"], + expectedNoAttributeInSource: ["C.op_AdditionAssignment"]); + } + + CreateCompilation([getLibSource("extern"), CompilerFeatureRequiredAttribute]).VerifyDiagnostics( + // (4,36): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // public extern void operator +=(int* p); + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(4, 36)); + + // When compiling the lib under legacy rules, extern members are not unsafe, but members with pointers are. + var libLegacy = CreateCompilation( + [getLibSource("unsafe extern"), CompilerFeatureRequiredAttribute], + options: TestOptions.UnsafeReleaseDll) + .VerifyDiagnostics(); + + foreach (var useCompilationReference in new[] { false, true }) + { + var libLegacyRef = AsReference(libLegacy, useCompilationReference); + + var libAssemblySymbol = CreateCompilation(callerSource, + [libLegacyRef], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (2,1): error CS9363: 'C.operator +=(int*)' must be used in an unsafe context because it has pointers in its signature + // c += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperationCompat, "c += null").WithArguments("C.operator +=(int*)").WithLocation(2, 1)) + .GetReferencedAssemblySymbol(libLegacyRef); + + VerifyRequiresUnsafeAttribute( + libAssemblySymbol.Modules.Single(), + expectedUnsafeSymbols: ["C.op_AdditionAssignment"], + expectedSafeSymbols: ["C"], + expectedUnsafeMode: CallerUnsafeMode.Implicit); + } + } + + [Fact] + public void Extern_Operator_Explicit() + { + CompileAndVerifyUnsafe( + lib: """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public extern void operator +=(C c); + } + """, + caller: """ + var c = new C(); + c += null; + """, + additionalSources: [CompilerFeatureRequiredAttribute, RequiresUnsafeAttributeDefinition], + verify: Verification.Skipped, + expectedUnsafeSymbols: ["C.op_AdditionAssignment"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'C.operator +=(C)' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c += null; + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c += null").WithArguments("C.operator +=(C)").WithLocation(2, 1), + ]); + } + + [Fact] + public void RequiresUnsafeAttribute_Applied() + { + var source = """ + class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void M1() { } + void M2() { } + } + """; + + var expectedDiagnostics = new[] + { + // (3,6): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(3, 6), + }; + + CreateCompilation([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithWarningLevel(10)) + .VerifyEmitDiagnostics(); + CreateCompilation([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithWarningLevel(11)) + .VerifyEmitDiagnostics(expectedDiagnostics); + + CompileAndVerify([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithMetadataImportOptions(MetadataImportOptions.All), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: ["C.M1"], + expectedSafeSymbols: ["C", "C.M2"], + expectedUnsafeMode: CallerUnsafeMode.None)) + .VerifyDiagnostics(expectedDiagnostics); + + var ref1 = CompileAndVerify([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules().WithMetadataImportOptions(MetadataImportOptions.All), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: ["C.M1"], + expectedSafeSymbols: ["C", "C.M2"])) + .VerifyDiagnostics() + .GetImageReference(); + + CompileAndVerify("", [ref1], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules().WithMetadataImportOptions(MetadataImportOptions.All), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [], + expectedSafeSymbols: [])) + .VerifyDiagnostics(); + + var source2 = """ + class B + { + void M3() { } + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void M4() { } + } + """; + + CompileAndVerify(source2, [ref1], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules().WithMetadataImportOptions(MetadataImportOptions.All), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: ["B.M4"], + expectedSafeSymbols: ["B", "B.M3"])) + .VerifyDiagnostics(); + + CompileAndVerify([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.ReleaseModule.WithAllowUnsafe(true).WithMetadataImportOptions(MetadataImportOptions.All), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: ["C.M1"], + expectedSafeSymbols: ["C", "C.M2"], + expectedUnsafeMode: CallerUnsafeMode.None)) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation([source, MemorySafetyRulesAttributeDefinition], + options: TestOptions.ReleaseModule.WithAllowUnsafe(true).WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,38): error CS0234: The type or namespace name 'RequiresUnsafeAttribute' does not exist in the namespace 'System.Diagnostics.CodeAnalysis' (are you missing an assembly reference?) + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_DottedTypeNameNotFoundInNS, "RequiresUnsafe").WithArguments("RequiresUnsafeAttribute", "System.Diagnostics.CodeAnalysis").WithLocation(3, 38), + // (3,38): error CS0234: The type or namespace name 'RequiresUnsafe' does not exist in the namespace 'System.Diagnostics.CodeAnalysis' (are you missing an assembly reference?) + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_DottedTypeNameNotFoundInNS, "RequiresUnsafe").WithArguments("RequiresUnsafe", "System.Diagnostics.CodeAnalysis").WithLocation(3, 38)); + } + + [Fact] + public void RequiresUnsafeAttribute_NotApplied() + { + var source = """ + public class C + { + public void M() { } + } + """; + + CompileAndVerify(source, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [], + expectedSafeSymbols: ["C", "C.M"])) + .VerifyDiagnostics(); + + CompileAndVerify(source, + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [], + expectedSafeSymbols: ["C", "C.M"])) + .VerifyDiagnostics(); + + CompileAndVerify([source, MemorySafetyRulesAttributeDefinition], + options: TestOptions.ReleaseModule.WithUpdatedMemorySafetyRules(), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [], + expectedSafeSymbols: ["C", "C.M"])) + .VerifyDiagnostics(); + } + + [Fact] + public void RequiresUnsafeAttribute_LocalFunction() + { + var source = """ + #pragma warning disable CS8321 // Local function is declared but never used + class C + { + void M() + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + void M1() { } + void M2() { } + } + } + """; + + var m1 = "C.g__M1|0_0"; + var m2 = "C.g__M2|0_1"; + + var expectedDiagnostics = new[] + { + // (6,10): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(6, 10), + }; + + CompileAndVerify([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithMetadataImportOptions(MetadataImportOptions.All), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [m1], + expectedSafeSymbols: [m2], + expectedUnsafeMode: CallerUnsafeMode.None)) + .VerifyDiagnostics(expectedDiagnostics); + + CompileAndVerify([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules().WithMetadataImportOptions(MetadataImportOptions.All), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [m1], + expectedSafeSymbols: [m2])) + .VerifyDiagnostics(); + + CompileAndVerify([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.ReleaseModule.WithAllowUnsafe(true).WithMetadataImportOptions(MetadataImportOptions.All), + verify: Verification.Skipped, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [m1], + expectedSafeSymbols: [m2], + expectedUnsafeMode: CallerUnsafeMode.None)) + .VerifyDiagnostics(expectedDiagnostics); + + CreateCompilation([source, MemorySafetyRulesAttributeDefinition], + options: TestOptions.ReleaseModule.WithAllowUnsafe(true).WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics( + // (6,42): error CS0234: The type or namespace name 'RequiresUnsafeAttribute' does not exist in the namespace 'System.Diagnostics.CodeAnalysis' (are you missing an assembly reference?) + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_DottedTypeNameNotFoundInNS, "RequiresUnsafe").WithArguments("RequiresUnsafeAttribute", "System.Diagnostics.CodeAnalysis").WithLocation(6, 42), + // (6,42): error CS0234: The type or namespace name 'RequiresUnsafe' does not exist in the namespace 'System.Diagnostics.CodeAnalysis' (are you missing an assembly reference?) + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_DottedTypeNameNotFoundInNS, "RequiresUnsafe").WithArguments("RequiresUnsafe", "System.Diagnostics.CodeAnalysis").WithLocation(6, 42)); + } + + [Fact] + public void RequiresUnsafeAttribute_Reflection() + { + var sourceA = """ + using System; + using System.Linq; + using System.Reflection; + public class A + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M1() { } + public void M2() { } + public static void RequiresUnsafe(MethodInfo method) + { + var count = method.GetCustomAttributes(inherit: false).Count(a => a.GetType().Name == "RequiresUnsafeAttribute"); + Console.Write(count); + } + } + """; + var refA = CreateCompilation([sourceA, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics() + .EmitToImageReference(); + + var sourceB = """ + class B : A + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M3() { } + public void M4() { } + static void Main() + { + RequiresUnsafe(typeof(A).GetMethod("M1")); + RequiresUnsafe(typeof(A).GetMethod("M2")); + RequiresUnsafe(typeof(B).GetMethod("M3")); + RequiresUnsafe(typeof(B).GetMethod("M4")); + } + } + """; + CompileAndVerify(sourceB, [refA], + options: TestOptions.UnsafeReleaseExe.WithUpdatedMemorySafetyRules(), + expectedOutput: "1010") + .VerifyDiagnostics(); + } + + [Fact] + public void RequiresUnsafeAttribute_FromSource() + { + var source = """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M() { } + } + """; + + CompileAndVerify([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"], + expectedUnsafeMode: CallerUnsafeMode.None)) + .VerifyDiagnostics( + // (3,6): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "System.Diagnostics.CodeAnalysis.RequiresUnsafe").WithLocation(3, 6)); + + CompileAndVerify([source, RequiresUnsafeAttributeDefinition], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"])) + .VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void RequiresUnsafeAttribute_FromMetadata(bool useCompilationReference) + { + var comp = CreateCompilation(RequiresUnsafeAttributeDefinition); + CompileAndVerify(comp, + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: [], + expectedSafeSymbols: [AttributeDescription.RequiresUnsafeAttribute.FullName])) + .VerifyDiagnostics(); + var ref1 = AsReference(comp, useCompilationReference); + + var source = """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M() { } + } + """; + + CompileAndVerify(source, [ref1], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules(), + symbolValidator: m => VerifyRequiresUnsafeAttribute( + m, + expectedUnsafeSymbols: ["C.M"], + expectedSafeSymbols: ["C"])) + .VerifyDiagnostics(); + } + + [Theory, CombinatorialData] + public void RequiresUnsafeAttribute_FromMetadata_Multiple(bool useCompilationReference) + { + var comp1 = CreateCompilation(RequiresUnsafeAttributeDefinition, assemblyName: "lib1").VerifyDiagnostics(); + var ref1 = AsReference(comp1, useCompilationReference); + + var comp2 = CreateCompilation(RequiresUnsafeAttributeDefinition, assemblyName: "lib2").VerifyDiagnostics(); + var ref2 = AsReference(comp2, useCompilationReference); + + var source = """ + public class C + { + [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + public void M() { } + } + """; + + // Ambiguous attribute definitions from references. + CreateCompilation(source, [ref1, ref2], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,38): error CS0433: The type 'RequiresUnsafeAttribute' exists in both 'lib1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' and 'lib2, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.ERR_SameFullNameAggAgg, "RequiresUnsafe").WithArguments("lib1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", "lib2, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null").WithLocation(3, 38)); + + // Also defined in source. + var lib = CreateCompilation([source, RequiresUnsafeAttributeDefinition], [ref1, ref2], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (3,38): warning CS0436: The type 'RequiresUnsafeAttribute' in '' conflicts with the imported type 'RequiresUnsafeAttribute' in 'lib1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'. Using the type defined in ''. + // [System.Diagnostics.CodeAnalysis.RequiresUnsafe] + Diagnostic(ErrorCode.WRN_SameFullNameThisAggAgg, "RequiresUnsafe").WithArguments("", "System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", "lib1, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null", "System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute").WithLocation(3, 38)); + + CreateCompilation(""" + new C().M(); + """, + [AsReference(lib, useCompilationReference)], + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,1): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // new C().M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C().M()").WithArguments("C.M()").WithLocation(1, 1)); + } + + [Theory, CombinatorialData] + public void RequiresUnsafeAttribute_FromMetadata_Multiple_AndCorLib(bool useCompilationReference) + { + var corlibSource = """ + namespace System + { + public class Object; + public class ValueType; + public class String; + public class Attribute; + public struct Void; + public struct Int32; + public struct Boolean; + public class AttributeUsageAttribute + { + public AttributeUsageAttribute(AttributeTargets t) { } + public bool AllowMultiple { get; set; } + public bool Inherited { get; set; } + } + public class Enum; + public enum AttributeTargets; + } + + namespace System.Diagnostics.CodeAnalysis + { + public sealed class RequiresUnsafeAttribute : Attribute; + } + """; + + var corlib = CreateEmptyCompilation(corlibSource, assemblyName: "corlib").VerifyDiagnostics(); + var corlibRef = AsReference(corlib, useCompilationReference); + + var comp1 = CreateEmptyCompilation("", [corlibRef], assemblyName: "lib1").VerifyDiagnostics(); + var ref1 = AsReference(comp1, useCompilationReference); + + var comp2 = CreateEmptyCompilation("", [corlibRef], assemblyName: "lib2").VerifyDiagnostics(); + var ref2 = AsReference(comp2, useCompilationReference); + + var source = """ + #pragma warning disable CS0626 // extern without attributes + public class C + { + public extern void M(); + } + """; + + // Using the attribute from corlib even if there are ambiguous definitions in other references. + var lib = CreateEmptyCompilation(source, [ref1, ref2, corlibRef], + options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules() + .WithSpecificDiagnosticOptions([KeyValuePair.Create("CS8021", ReportDiagnostic.Suppress)])) + .VerifyDiagnostics(); + + CreateEmptyCompilation(""" + new C().M(); + """, + [AsReference(lib, useCompilationReference), ref1, ref2, corlibRef], + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,1): error CS9362: 'C.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // new C().M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "new C().M()").WithArguments("C.M()").WithLocation(1, 1)); + } + + [Fact] + public void RequiresUnsafeAttribute_FromMetadata_UnrecognizedConstructor() + { + // [module: MemorySafetyRules(2)] + // public class A + // { + // [RequiresUnsafe(1), RequiresUnsafe(0)] + // public static void M() => throw null; + // } + var sourceA = $$""" + .assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) } + .assembly '<>' { } + .module '<>.dll' + .custom instance void System.Runtime.CompilerServices.MemorySafetyRulesAttribute::.ctor(int32) = { int32({{CSharpCompilationOptions.UpdatedMemorySafetyRulesVersion}}) } + .class private System.Runtime.CompilerServices.MemorySafetyRulesAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(int32 version) cil managed { ret } + } + .class private System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(int32 version) cil managed { ret } + } + .class public A + { + .method public static void M() + { + .custom instance void System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute::.ctor(int32) = { int32(1) } + .custom instance void System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute::.ctor(int32) = { int32(0) } + ldnull throw + } + } + """; + var refA = CompileIL(sourceA, prependDefaultHeader: false); + + var a = CreateCompilation("", [refA]).VerifyDiagnostics().GetReferencedAssemblySymbol(refA); + Assert.Equal(CallerUnsafeMode.None, a.GlobalNamespace.GetMember("A.M").CallerUnsafeMode); + + var sourceB = """ + A.M(); + """; + CreateCompilation(sourceB, [refA], + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyEmitDiagnostics(); + } + + [Fact] + public void RequiresUnsafeAttribute_FromMetadata_UnrecognizedAndRecognizedConstructor() + { + // [module: MemorySafetyRules(2)] + // public class A + // { + // [RequiresUnsafe(1), RequiresUnsafe()] + // public static void M() => throw null; + // } + var sourceA = $$""" + .assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) } + .assembly '<>' { } + .module '<>.dll' + .custom instance void System.Runtime.CompilerServices.MemorySafetyRulesAttribute::.ctor(int32) = { int32({{CSharpCompilationOptions.UpdatedMemorySafetyRulesVersion}}) } + .class private System.Runtime.CompilerServices.MemorySafetyRulesAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(int32 version) cil managed { ret } + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ret } + } + .class private System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(int32 version) cil managed { ret } + } + .class public A + { + .method public static void M() + { + .custom instance void System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute::.ctor(int32) = { int32(1) } + .custom instance void System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute::.ctor() + ldnull throw + } + } + """; + var refA = CompileIL(sourceA, prependDefaultHeader: false); + + var a = CreateCompilation("", [refA]).VerifyDiagnostics().GetReferencedAssemblySymbol(refA); + Assert.Equal(CallerUnsafeMode.Explicit, a.GlobalNamespace.GetMember("A.M").CallerUnsafeMode); + + var sourceB = """ + A.M(); + """; + CreateCompilation(sourceB, [refA], + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,1): error CS9362: 'A.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // A.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "A.M()").WithArguments("A.M()").WithLocation(1, 1)); + } + + [Fact] + public void RequiresUnsafeAttribute_FromMetadata_AppliedMultipleTimes() + { + // [module: MemorySafetyRules(2)] + // public class A + // { + // [RequiresUnsafe, RequiresUnsafe] + // public static void M() => throw null; + // } + var sourceA = $$""" + .assembly extern mscorlib { .ver 4:0:0:0 .publickeytoken = (B7 7A 5C 56 19 34 E0 89) } + .assembly '<>' { } + .module '<>.dll' + .custom instance void System.Runtime.CompilerServices.MemorySafetyRulesAttribute::.ctor(int32) = { int32({{CSharpCompilationOptions.UpdatedMemorySafetyRulesVersion}}) } + .class private System.Runtime.CompilerServices.MemorySafetyRulesAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor(int32 version) cil managed { ret } + } + .class private System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute extends [mscorlib]System.Attribute + { + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ret } + } + .class public A + { + .method public static void M() + { + .custom instance void System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute::.ctor() + .custom instance void System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute::.ctor() + ldnull throw + } + } + """; + var refA = CompileIL(sourceA, prependDefaultHeader: false); + + var a = CreateCompilation("", [refA]).VerifyDiagnostics().GetReferencedAssemblySymbol(refA); + Assert.Equal(CallerUnsafeMode.Explicit, a.GlobalNamespace.GetMember("A.M").CallerUnsafeMode); + + var sourceB = """ + A.M(); + """; + CreateCompilation(sourceB, [refA], + options: TestOptions.ReleaseExe.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (1,1): error CS9362: 'A.M()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // A.M(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "A.M()").WithArguments("A.M()").WithLocation(1, 1)); + } + + [Theory, CombinatorialData] + public void RequiresUnsafeAttribute_ReferencedInSource( + bool updatedRules, + bool useCompilationReference) + { + var comp1 = CreateCompilation(""" + namespace System.Diagnostics.CodeAnalysis + { + public sealed class RequiresUnsafeAttribute : Attribute; + } + """).VerifyDiagnostics(); + var ref1 = AsReference(comp1, useCompilationReference); + + CSharpTestSource source = + [ + """ + using System.Diagnostics.CodeAnalysis; + [RequiresUnsafeAttribute] class C + { + [RequiresUnsafeAttribute] void M() { } + [RequiresUnsafeAttribute] int P1 { get; set; } + [field: RequiresUnsafeAttribute] int P2 { get; set; } + int P3 { [RequiresUnsafeAttribute] get; [RequiresUnsafeAttribute] set; } + #pragma warning disable CS0067 // unused event + [RequiresUnsafeAttribute] event System.Action E1; + [field: RequiresUnsafeAttribute] event System.Action E2; + event System.Action E3 { [RequiresUnsafeAttribute] add { } [RequiresUnsafeAttribute] remove { } } + [RequiresUnsafeAttribute] int this[int i] { get => i; set { } } + [RequiresUnsafeAttribute] C() { } + [RequiresUnsafeAttribute] ~C() { } + [RequiresUnsafeAttribute] static C() { } + [RequiresUnsafeAttribute] public static C operator +(C c1, C c2) => c1; + [RequiresUnsafeAttribute] public void operator +=(C c) { } + public void M([RequiresUnsafeAttribute] int x) { } + [return: RequiresUnsafeAttribute] public int Func() => 0; + public void M<[RequiresUnsafeAttribute] T>() { } + #pragma warning disable CS0169 // unused field + [RequiresUnsafeAttribute] int F; + void L() { var lam = [RequiresUnsafeAttribute] () => { }; } + } + [RequiresUnsafeAttribute] delegate void D(); + [RequiresUnsafeAttribute] enum E { X } + """, """ + using System.Diagnostics.CodeAnalysis; + [module: RequiresUnsafeAttribute] + [assembly: RequiresUnsafeAttribute] + """, + CompilerFeatureRequiredAttribute, + ]; + + var commonErrors = new[] + { + // (14,6): error CS9367: RequiresUnsafeAttribute cannot be applied to this symbol. + // [RequiresUnsafeAttribute] ~C() { } + Diagnostic(ErrorCode.ERR_RequiresUnsafeAttributeUnsupportedMemberTarget, "RequiresUnsafeAttribute").WithLocation(14, 6), + // (15,6): error CS9367: RequiresUnsafeAttribute cannot be applied to this symbol. + // [RequiresUnsafeAttribute] static C() { } + Diagnostic(ErrorCode.ERR_RequiresUnsafeAttributeUnsupportedMemberTarget, "RequiresUnsafeAttribute").WithLocation(15, 6), + // (23,27): error CS9367: RequiresUnsafeAttribute cannot be applied to this symbol. + // void L() { var lam = [RequiresUnsafeAttribute] () => { }; } + Diagnostic(ErrorCode.ERR_RequiresUnsafeAttributeUnsupportedMemberTarget, "RequiresUnsafeAttribute").WithLocation(23, 27), + }; + + var commonWarnings = new[] + { + // (4,6): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // [RequiresUnsafeAttribute] void M() { } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "RequiresUnsafeAttribute").WithLocation(4, 6), + // (7,15): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // int P3 { [RequiresUnsafeAttribute] get; [RequiresUnsafeAttribute] set; } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "RequiresUnsafeAttribute").WithLocation(7, 15), + // (7,46): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // int P3 { [RequiresUnsafeAttribute] get; [RequiresUnsafeAttribute] set; } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "RequiresUnsafeAttribute").WithLocation(7, 46), + // (11,31): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // event System.Action E3 { [RequiresUnsafeAttribute] add { } [RequiresUnsafeAttribute] remove { } } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "RequiresUnsafeAttribute").WithLocation(11, 31), + // (11,65): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // event System.Action E3 { [RequiresUnsafeAttribute] add { } [RequiresUnsafeAttribute] remove { } } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "RequiresUnsafeAttribute").WithLocation(11, 65), + // (13,6): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // [RequiresUnsafeAttribute] C() { } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "RequiresUnsafeAttribute").WithLocation(13, 6), + // (16,6): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // [RequiresUnsafeAttribute] public static C operator +(C c1, C c2) => c1; + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "RequiresUnsafeAttribute").WithLocation(16, 6), + // (17,6): warning CS9368: RequiresUnsafeAttribute is only valid under the updated memory safety rules. + // [RequiresUnsafeAttribute] public void operator +=(C c) { } + Diagnostic(ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules, "RequiresUnsafeAttribute").WithLocation(17, 6), + }; + + CreateCompilation(source, [ref1], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(updatedRules)) + .VerifyDiagnostics( + [ + .. commonErrors, + .. (updatedRules ? default(ReadOnlySpan) : commonWarnings), + ]); + + var comp2 = CreateCompilation(RequiresUnsafeAttributeDefinition).VerifyDiagnostics(); + var ref2 = AsReference(comp2, useCompilationReference); + + CreateCompilation(source, [ref2], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules(updatedRules)) + .VerifyDiagnostics( + [ + .. commonErrors, + .. (updatedRules ? default(ReadOnlySpan) : commonWarnings), + // (3,12): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [assembly: RequiresUnsafeAttribute] + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(3, 12), + // (2,10): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [module: RequiresUnsafeAttribute] + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(2, 10), + // (2,2): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [RequiresUnsafeAttribute] class C + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(2, 2), + // (25,2): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [RequiresUnsafeAttribute] delegate void D(); + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(25, 2), + // (26,2): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [RequiresUnsafeAttribute] enum E { X } + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(26, 2), + // (18,20): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // public void M([RequiresUnsafeAttribute] int x) { } + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(18, 20), + // (20,20): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // public void M<[RequiresUnsafeAttribute] T>() { } + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(20, 20), + // (6,13): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [field: RequiresUnsafeAttribute] int P2 { get; set; } + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(6, 13), + // (10,13): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [field: RequiresUnsafeAttribute] event System.Action E2; + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(10, 13), + // (19,14): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [return: RequiresUnsafeAttribute] public int Func() => 0; + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(19, 14), + // (22,6): error CS0592: Attribute 'RequiresUnsafeAttribute' is not valid on this declaration type. It is only valid on 'constructor, method, property, indexer, event' declarations. + // [RequiresUnsafeAttribute] int F; + Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "RequiresUnsafeAttribute").WithArguments("RequiresUnsafeAttribute", "constructor, method, property, indexer, event").WithLocation(22, 6), + ]); + } + + [Fact] + public void RequiresUnsafeAttribute_Partial() + { + CompileAndVerifyUnsafe( + lib: """ + using System.Diagnostics.CodeAnalysis; + public partial class C + { + [RequiresUnsafe] public partial int M1(); + public partial int M1() => 0; + + public partial int M2(); + [RequiresUnsafe] public partial int M2() => 0; + } + """, + caller: """ + var c = new C(); + c.M1(); + c.M2(); + """, + additionalSources: [RequiresUnsafeAttributeDefinition], + expectedUnsafeSymbols: ["C.M1", "C.M2"], + expectedSafeSymbols: ["C"], + expectedDiagnostics: + [ + // (2,1): error CS9362: 'C.M1()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M1(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M1()").WithArguments("C.M1()").WithLocation(2, 1), + // (3,1): error CS9362: 'C.M2()' must be used in an unsafe context because it is marked as 'RequiresUnsafe' or 'extern' + // c.M2(); + Diagnostic(ErrorCode.ERR_UnsafeMemberOperation, "c.M2()").WithArguments("C.M2()").WithLocation(3, 1), + ]); + + CreateCompilation( + [ + """ + using System.Diagnostics.CodeAnalysis; + partial class C + { + [RequiresUnsafe] public partial int M(); + [RequiresUnsafe] public partial int M() => 0; + } + """, + RequiresUnsafeAttributeDefinition, + ], + options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()) + .VerifyDiagnostics( + // (5,6): error CS0579: Duplicate 'RequiresUnsafe' attribute + // [RequiresUnsafe] public partial int M() => 0; + Diagnostic(ErrorCode.ERR_DuplicateAttribute, "RequiresUnsafe").WithArguments("RequiresUnsafe").WithLocation(5, 6)); + } +} diff --git a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs index 02c3491acd28..47cca4ca8459 100644 --- a/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/CommandLineTests.cs @@ -1722,6 +1722,9 @@ public void LanguageVersionAdded_Canary() // - [ ] replace all references to C# "Next" (such as `TestOptions.RegularNext` or `LanguageVersionFacts.CSharpNext`) with the new version and fix failing tests // - [ ] update _MaxAvailableLangVersion cap (a relevant test should break when new version is introduced) // - [ ] update the "UpgradeProject" codefixer + // - [ ] Remove the `ExperimentalUrl` section from any entries for language features being shipped in Syntax.xml and OperationInterfaces.xml, and rerun the generator + // - [ ] Search the codebase for references tied to issues linked from Syntax.xml and OperationInterfaces.xml, and remove suppressions or attributes added for those issues + // - [ ] Search for any remaining uses of `Experimental` on APIs and remove if the feature is being shipped in stable form // - [ ] test VS insertion and deal with breaking changes. (note: the runtime repo uses "preview" so breaks are resolved sooner) // // Other repos also need updates: @@ -4870,7 +4873,7 @@ public void Nullable() parsedArgs = DefaultParse(new[] { @"/nullable+", "/langversion:7.0", "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify( // error CS8630: Invalid 'nullable' value: 'Enabled' for C# 7.0. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("nullable", "Enable", "7.0", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("nullable", "Enable", "7.0", "8.0").WithLocation(1, 1) ); Assert.Equal(NullableContextOptions.Enable, parsedArgs.CompilationOptions.NullableContextOptions); @@ -4899,7 +4902,7 @@ public void Nullable() parsedArgs = DefaultParse(new[] { @"/nullable:enable", "/langversion:7.0", "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify( // error CS8630: Invalid 'nullable' value: 'Enable' for C# 7.0. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("nullable", "Enable", "7.0", "8.0").WithLocation(1, 1)); + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("nullable", "Enable", "7.0", "8.0").WithLocation(1, 1)); Assert.Equal(NullableContextOptions.Enable, parsedArgs.CompilationOptions.NullableContextOptions); parsedArgs = DefaultParse(new[] { @"/nullable:disable", "/langversion:7.0", "a.cs" }, WorkingDirectory); @@ -5110,7 +5113,7 @@ public void Nullable() parsedArgs = DefaultParse(new[] { @"/nullable+", "/langversion:7.3", "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify( // error CS8630: Invalid 'nullable' value: 'Enable' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("nullable", "Enable", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("nullable", "Enable", "7.3", "8.0").WithLocation(1, 1) ); Assert.Equal(NullableContextOptions.Enable, parsedArgs.CompilationOptions.NullableContextOptions); @@ -5121,14 +5124,14 @@ public void Nullable() parsedArgs = DefaultParse(new[] { @"/nullable", "/langversion:7.3", "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify( // error CS8630: Invalid 'nullable' value: 'Enabled' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("nullable", "Enable", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("nullable", "Enable", "7.3", "8.0").WithLocation(1, 1) ); Assert.Equal(NullableContextOptions.Enable, parsedArgs.CompilationOptions.NullableContextOptions); parsedArgs = DefaultParse(new[] { @"/nullable:enable", "/langversion:7.3", "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify( // error CS8630: Invalid 'nullable' value: 'Enabled' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("nullable", "Enable", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("nullable", "Enable", "7.3", "8.0").WithLocation(1, 1) ); Assert.Equal(NullableContextOptions.Enable, parsedArgs.CompilationOptions.NullableContextOptions); @@ -5203,7 +5206,7 @@ public void Nullable() parsedArgs = DefaultParse(new[] { @"/nullable:warnings", "/langversion:7.0", "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify( // error CS8630: Invalid 'nullable' value: 'Warnings' for C# 7.0. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("nullable", "Warnings", "7.0", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("nullable", "Warnings", "7.0", "8.0").WithLocation(1, 1) ); Assert.Equal(NullableContextOptions.Warnings, parsedArgs.CompilationOptions.NullableContextOptions); @@ -5260,14 +5263,14 @@ public void Nullable() parsedArgs = DefaultParse(new[] { @"/nullable:Warnings", "/langversion:7.3", "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify( // error CS8630: Invalid 'nullable' value: 'Annotations' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("nullable", "Warnings", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("nullable", "Warnings", "7.3", "8.0").WithLocation(1, 1) ); Assert.Equal(NullableContextOptions.Warnings, parsedArgs.CompilationOptions.NullableContextOptions); parsedArgs = DefaultParse(new[] { @"/nullable:annotations", "/langversion:7.0", "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify( // error CS8630: Invalid 'nullable' value: 'Annotations' for C# 7.0. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("nullable", "Annotations", "7.0", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("nullable", "Annotations", "7.0", "8.0").WithLocation(1, 1) ); Assert.Equal(NullableContextOptions.Annotations, parsedArgs.CompilationOptions.NullableContextOptions); @@ -5324,7 +5327,7 @@ public void Nullable() parsedArgs = DefaultParse(new[] { @"/nullable:Annotations", "/langversion:7.3", "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify( // error CS8630: Invalid 'nullable' value: 'Annotations' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("nullable", "Annotations", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("nullable", "Annotations", "7.3", "8.0").WithLocation(1, 1) ); Assert.Equal(NullableContextOptions.Annotations, parsedArgs.CompilationOptions.NullableContextOptions); } @@ -5831,6 +5834,16 @@ public void ChecksumAlgorithm() Assert.Equal(SourceHashAlgorithm.Sha256, parsedArgs.ChecksumAlgorithm); Assert.Equal(HashAlgorithmName.SHA256, parsedArgs.EmitOptions.PdbChecksumAlgorithm); + parsedArgs = DefaultParse(["/checksumAlgorithm:sHa384", "a.cs"], WorkingDirectory); + parsedArgs.Errors.Verify(); + Assert.Equal(SourceHashAlgorithm.Sha384, parsedArgs.ChecksumAlgorithm); + Assert.Equal(HashAlgorithmName.SHA256, parsedArgs.EmitOptions.PdbChecksumAlgorithm); + + parsedArgs = DefaultParse(["/checksumAlgorithm:sha512", "a.cs"], WorkingDirectory); + parsedArgs.Errors.Verify(); + Assert.Equal(SourceHashAlgorithm.Sha512, parsedArgs.ChecksumAlgorithm); + Assert.Equal(HashAlgorithmName.SHA256, parsedArgs.EmitOptions.PdbChecksumAlgorithm); + parsedArgs = DefaultParse(new[] { "a.cs" }, WorkingDirectory); parsedArgs.Errors.Verify(); @@ -9434,7 +9447,7 @@ public void ReportAnalyzerOutput() responseFile: null, srcDirectory, new[] { "/reportanalyzer", "/t:library", srcFile.Path }, - analyzers: [new WarningDiagnosticAnalyzer(), new DiagnosticSuppressorForId("Warning01", "Suppressor01")], + analyzers: [new WarningDiagnosticAnalyzer(), new ConcurrentAnalyzer(["C"]), new DiagnosticSuppressorForId("Warning01", "Suppressor01")], generators: new[] { new DoNothingGenerator().AsSourceGenerator() }); var exitCode = csc.Run(outWriter); Assert.Equal(0, exitCode); @@ -9444,6 +9457,67 @@ public void ReportAnalyzerOutput() Assert.Contains($"{nameof(DiagnosticSuppressorForId)} (Suppressor01)", output, StringComparison.Ordinal); Assert.Contains(CodeAnalysisResources.GeneratorNameColumnHeader, output, StringComparison.Ordinal); Assert.Contains(typeof(DoNothingGenerator).FullName, output, StringComparison.Ordinal); + + Assert.DoesNotContain(CodeAnalysisResources.AllAnalyzersConcurrentMessage, output, StringComparison.Ordinal); + Assert.Contains(string.Format(CodeAnalysisResources.SuppressorsNonConcurrentCountMessage, 1), output, StringComparison.Ordinal); + var nonConcurrentSection = output[output.IndexOf(CodeAnalysisResources.NonConcurrentAnalyzersHeader)..]; + Assert.Contains(nameof(WarningDiagnosticAnalyzer), nonConcurrentSection, StringComparison.Ordinal); + Assert.DoesNotContain(nameof(DiagnosticSuppressorForId), nonConcurrentSection, StringComparison.Ordinal); + Assert.DoesNotContain(typeof(ConcurrentAnalyzer).Assembly.FullName, nonConcurrentSection, StringComparison.Ordinal); + Assert.DoesNotContain(nameof(ConcurrentAnalyzer), nonConcurrentSection, StringComparison.Ordinal); + CleanupAllGeneratedFiles(srcFile.Path); + } + + [Fact] + public void ReportAnalyzerOutput_AllConcurrentAnalyzers() + { + var srcFile = Temp.CreateFile().WriteAllText(@"class C {}"); + var srcDirectory = Path.GetDirectoryName(srcFile.Path); + + var outWriter = new StringWriter(CultureInfo.InvariantCulture); + var csc = CreateCSharpCompiler( + responseFile: null, + srcDirectory, + new[] { "/reportanalyzer", "/t:library", srcFile.Path }, + analyzers: [new ConcurrentAnalyzer(["C"]), new DiagnosticSuppressorForId("Warning01", "Suppressor01")], + generators: new[] { new DoNothingGenerator().AsSourceGenerator() }); + var exitCode = csc.Run(outWriter); + Assert.Equal(0, exitCode); + var output = outWriter.ToString(); + Assert.Contains(CodeAnalysisResources.AnalyzerExecutionTimeColumnHeader, output, StringComparison.Ordinal); + Assert.Contains($"{nameof(DiagnosticSuppressorForId)} (Suppressor01)", output, StringComparison.Ordinal); + Assert.Contains(CodeAnalysisResources.GeneratorNameColumnHeader, output, StringComparison.Ordinal); + Assert.Contains(typeof(DoNothingGenerator).FullName, output, StringComparison.Ordinal); + + Assert.Contains(string.Format(CodeAnalysisResources.SuppressorsNonConcurrentCountMessage, 1), output, StringComparison.Ordinal); + Assert.DoesNotContain(CodeAnalysisResources.NonConcurrentAnalyzersHeader, output, StringComparison.Ordinal); + Assert.Contains(CodeAnalysisResources.AllAnalyzersConcurrentMessage, output, StringComparison.Ordinal); + CleanupAllGeneratedFiles(srcFile.Path); + } + + [Fact] + public void ReportAnalyzerOutput_AllConcurrentAnalyzers_NoSuppressors() + { + var srcFile = Temp.CreateFile().WriteAllText(@"class C {}"); + var srcDirectory = Path.GetDirectoryName(srcFile.Path); + + var outWriter = new StringWriter(CultureInfo.InvariantCulture); + var csc = CreateCSharpCompiler( + responseFile: null, + srcDirectory, + new[] { "/reportanalyzer", "/t:library", srcFile.Path }, + analyzers: [new ConcurrentAnalyzer(["C"])], + generators: new[] { new DoNothingGenerator().AsSourceGenerator() }); + var exitCode = csc.Run(outWriter); + Assert.Equal(0, exitCode); + var output = outWriter.ToString(); + Assert.Contains(CodeAnalysisResources.AnalyzerExecutionTimeColumnHeader, output, StringComparison.Ordinal); + Assert.Contains(CodeAnalysisResources.GeneratorNameColumnHeader, output, StringComparison.Ordinal); + Assert.Contains(typeof(DoNothingGenerator).FullName, output, StringComparison.Ordinal); + + Assert.DoesNotContain(string.Format(CodeAnalysisResources.SuppressorsNonConcurrentCountMessage, 0), output, StringComparison.Ordinal); + Assert.DoesNotContain(CodeAnalysisResources.NonConcurrentAnalyzersHeader, output, StringComparison.Ordinal); + Assert.Contains(CodeAnalysisResources.AllAnalyzersConcurrentMessage, output, StringComparison.Ordinal); CleanupAllGeneratedFiles(srcFile.Path); } @@ -14821,7 +14895,7 @@ class C """); var cmd = CreateCSharpCompiler(null, dir.Path, - new[] { "/t:library", "/nologo", "/warnaserror+", src.Path }, + new[] { "/t:library", "/preferreduilang:en", "/nologo", "/warnaserror+", src.Path }, generators: new[] { new FailsExecuteGenerator() }); var outWriter = new StringWriter(CultureInfo.InvariantCulture); var exitCode = cmd.Run(outWriter); @@ -14866,7 +14940,7 @@ class C """); var cmd = CreateCSharpCompiler(null, dir.Path, - new[] { "/t:library", "/nologo", "/warnaserror+", src.Path }, + new[] { "/t:library", "/preferreduilang:en", "/nologo", "/warnaserror+", src.Path }, generators: new[] { new FailsInitializeGenerator() }); var outWriter = new StringWriter(CultureInfo.InvariantCulture); var exitCode = cmd.Run(outWriter); diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncSpillTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncSpillTests.cs index 6fc12c66a935..ea05fd081873 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncSpillTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncSpillTests.cs @@ -8514,10 +8514,11 @@ .locals init (S V_0) """); } - [Fact] - public void SpillAssignmentToThisStruct_02() + [Theory] + [CombinatorialData] + public void SpillAssignmentToThisStruct_02(bool disableRuntimeAsync) { - var source = """ + var source = $$""" using System; using System.Threading.Tasks; struct S : I @@ -8543,6 +8544,7 @@ static class Extensions { extension(T t) where T : I { + {{(disableRuntimeAsync ? "[System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)]" : "")}} public async Task M() { t.P = 1; @@ -8553,30 +8555,38 @@ public async Task M() """; var expectedOutput = "0"; - CompileAndVerify(source, expectedOutput: expectedOutput, options: TestOptions.ReleaseExe); - CompileAndVerify(source, expectedOutput: expectedOutput, options: TestOptions.DebugExe); + CompileAndVerify([source, RuntimeAsyncMethodGenerationAttributeDefinition], expectedOutput: expectedOutput, options: TestOptions.ReleaseExe); + CompileAndVerify([source, RuntimeAsyncMethodGenerationAttributeDefinition], expectedOutput: expectedOutput, options: TestOptions.DebugExe); - var comp = CreateRuntimeAsyncCompilation(source, TestOptions.ReleaseExe); + var comp = CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition], TestOptions.ReleaseExe); var verifier = CompileAndVerify(comp, expectedOutput: RuntimeAsyncTestHelpers.ExpectedOutput(expectedOutput), verify: Verification.Fails with { - ILVerifyMessage = """ + ILVerifyMessage = disableRuntimeAsync + ? """ + [Main]: Return value missing on the stack. { Offset = 0x1f } + """ + : """ [Main]: Return value missing on the stack. { Offset = 0x1f } [M]: Return value missing on the stack. { Offset = 0xe } """ }); verifier.VerifyDiagnostics(); - verifier.VerifyIL("Extensions.M(this T)", """ - { - // Code size 15 (0xf) - .maxstack 2 - IL_0000: ldarga.s V_0 - IL_0002: ldc.i4.1 - IL_0003: constrained. "T" - IL_0009: callvirt "void I.P.set" - IL_000e: ret - } - """); + if (!disableRuntimeAsync) + { + + verifier.VerifyIL("Extensions.M(this T)", """ + { + // Code size 15 (0xf) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: ldc.i4.1 + IL_0003: constrained. "T" + IL_0009: callvirt "void I.P.set" + IL_000e: ret + } + """); + } } [Fact] diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs index 5c9a010ccf82..a77077f46b2d 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAsyncTests.cs @@ -8568,6 +8568,410 @@ .maxstack 1 """); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82227")] + public void RuntimeAsync_SequencePoints_AsyncLambdaAndLocalFunction() + { + var source = """ + using System; + using System.Threading.Tasks; + + class Program + { + static void Main() + { + async Task LocalAsync() + { + await Task.CompletedTask; + } + + LocalAsync().Wait(); + + Func f = async () => + { + await Task.CompletedTask; + }; + + f().Wait(); + } + } + """; + + var comp = CreateRuntimeAsyncCompilation(source, options: TestOptions.DebugDll); + var verifier = CompileAndVerify(comp, verify: Verification.Fails with + { + ILVerifyMessage = $""" + {ReturnValueMissing("
g__LocalAsync|0_0", "0xc")} + {ReturnValueMissing("
b__0_1", "0xc")} + """ + }); + + verifier.VerifyIL("Program.
g__LocalAsync|0_0()", """ + { + // Code size 13 (0xd) + .maxstack 1 + // sequence point: { + IL_0000: nop + // sequence point: await Task.CompletedTask; + IL_0001: call "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get" + IL_0006: call "void System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.Task)" + IL_000b: nop + // sequence point: } + IL_000c: ret + } + """, sequencePointDisplay: SequencePointDisplayMode.Enhanced); + + verifier.VerifyIL("Program.<>c.
b__0_1()", """ + { + // Code size 13 (0xd) + .maxstack 1 + // sequence point: { + IL_0000: nop + // sequence point: await Task.CompletedTask; + IL_0001: call "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get" + IL_0006: call "void System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.Task)" + IL_000b: nop + // sequence point: } + IL_000c: ret + } + """, sequencePointDisplay: SequencePointDisplayMode.Enhanced); + } + + [Fact] + public void RuntimeAsync_SequencePoints_ImplicitReturn() + { + var source = """ + using System.Threading.Tasks; + + class Program + { + static async Task Main() + { + await Task.CompletedTask; + } + } + """; + + var comp = CreateRuntimeAsyncCompilation(source, options: TestOptions.DebugDll); + var verifier = CompileAndVerify(comp, verify: Verification.Fails with { ILVerifyMessage = ReturnValueMissing("Main", "0xc") }); + + verifier.VerifyIL("Program.Main()", """ + { + // Code size 13 (0xd) + .maxstack 1 + // sequence point: { + IL_0000: nop + // sequence point: await Task.CompletedTask; + IL_0001: call "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get" + IL_0006: call "void System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.Task)" + IL_000b: nop + // sequence point: } + IL_000c: ret + } + """, sequencePointDisplay: SequencePointDisplayMode.Enhanced); + } + + [Fact] + public void RuntimeAsync_SequencePoints_ClosingBrace() + { + var source = """ + using System.Threading.Tasks; + + class Program + { + static async Task Main() + { + await Task.CompletedTask; + System.Console.WriteLine(1); + } + } + """; + + var comp = CreateRuntimeAsyncCompilation(source, options: TestOptions.DebugDll); + var verifier = CompileAndVerify(comp, verify: Verification.Fails with { ILVerifyMessage = ReturnValueMissing("Main", "0x13") }); + + verifier.VerifyIL("Program.Main()", """ + { + // Code size 20 (0x14) + .maxstack 1 + // sequence point: { + IL_0000: nop + // sequence point: await Task.CompletedTask; + IL_0001: call "System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get" + IL_0006: call "void System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.Task)" + IL_000b: nop + // sequence point: System.Console.WriteLine(1); + IL_000c: ldc.i4.1 + IL_000d: call "void System.Console.WriteLine(int)" + IL_0012: nop + // sequence point: } + IL_0013: ret + } + """, sequencePointDisplay: SequencePointDisplayMode.Enhanced); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82274")] + public void RuntimeAsync_SequencePoints_CustomAwaiterBranch() + { + var source = """ + using System.Threading.Tasks; + + class Program + { + static async Task TaskReturningMethodAsync(bool syncReturn) + { + if (syncReturn) + return; + + await Task.Yield(); + } + } + """; + + var comp = CreateRuntimeAsyncCompilation(source, options: TestOptions.DebugDll); + var verifier = CompileAndVerify(comp, verify: Verification.Fails with { ILVerifyMessage = ReturnValueMissing("TaskReturningMethodAsync", "0x2e") }); + verifier.VerifyIL("Program.TaskReturningMethodAsync(bool)", """ + { + // Code size 47 (0x2f) + .maxstack 1 + .locals init (bool V_0, + System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter V_1, + System.Runtime.CompilerServices.YieldAwaitable V_2) + // sequence point: { + IL_0000: nop + // sequence point: if (syncReturn) + IL_0001: ldarg.0 + IL_0002: stloc.0 + // sequence point: + IL_0003: ldloc.0 + IL_0004: brfalse.s IL_0008 + // sequence point: return; + IL_0006: br.s IL_002e + // sequence point: await Task.Yield(); + IL_0008: call "System.Runtime.CompilerServices.YieldAwaitable System.Threading.Tasks.Task.Yield()" + IL_000d: stloc.2 + IL_000e: ldloca.s V_2 + IL_0010: call "System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter System.Runtime.CompilerServices.YieldAwaitable.GetAwaiter()" + IL_0015: stloc.1 + // sequence point: + IL_0016: ldloca.s V_1 + IL_0018: call "bool System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter.IsCompleted.get" + IL_001d: brtrue.s IL_0026 + IL_001f: ldloc.1 + IL_0020: call "void System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiter(System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter)" + IL_0025: nop + IL_0026: ldloca.s V_1 + IL_0028: call "void System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter.GetResult()" + IL_002d: nop + // sequence point: } + IL_002e: ret + } + """, sequencePointDisplay: SequencePointDisplayMode.Enhanced); + } + + [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/82551")] + [CombinatorialData] + public void RuntimeAsync_AwaitTaskWhenAll_AsyncLambdaInSelect_InferenceScenarios(bool explicitReturnType, bool statementBody, bool runtimeAsyncEnabledOnLambda) + { + var lambda = (explicitReturnType, statementBody) switch + { + (true, true) => "async Task (int x) => { return await Task.FromResult(x); }", + (true, false) => "async Task (int x) => await Task.FromResult(x)", + (false, true) => "async (x) => { return await Task.FromResult(x); }", + (false, false) => "async (x) => await Task.FromResult(x)", + }; + var lambdaRuntimeAsyncAttribute = $"[System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute({(runtimeAsyncEnabledOnLambda ? "true" : "false")})] "; + + var source = $$""" + using System.Linq; + using System.Threading.Tasks; + + class Program + { + static async Task Main() + { + await Task.WhenAll(new[] { 1, 2, 3 }.Select({{lambdaRuntimeAsyncAttribute}}{{lambda}})); + } + } + """; + + var comp = CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); + comp.VerifyEmitDiagnostics(); + } + + [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/82551")] + [CombinatorialData] + public void RuntimeAsync_LambdaReturnInference_InferenceScenarios_CustomTaskLikeContainingMethod(bool explicitReturnType, bool statementBody, bool runtimeAsyncEnabledOnLambda) + { + var lambda = (explicitReturnType, statementBody) switch + { + (true, true) => "async Task (int x) => { return await Task.FromResult(x); }", + (true, false) => "async Task (int x) => await Task.FromResult(x)", + (false, true) => "async (x) => { return await Task.FromResult(x); }", + (false, false) => "async (x) => await Task.FromResult(x)", + }; + var lambdaRuntimeAsyncAttribute = $"[System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute({(runtimeAsyncEnabledOnLambda ? "true" : "false")})] "; + + var source = $$""" + using System; + using System.Linq; + using System.Runtime.CompilerServices; + using System.Threading.Tasks; + + [AsyncMethodBuilder(typeof(MyTaskBuilder))] + class MyTask + { + } + + class MyTaskBuilder + { + public static MyTaskBuilder Create() => null; + public MyTask Task => null; + public void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine { } + public void SetStateMachine(IAsyncStateMachine stateMachine) { } + public void SetResult() { } + public void SetException(Exception exception) { } + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + } + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + } + } + + class Program + { + static async MyTask Main() + { + await Task.WhenAll(new[] { 1, 2, 3 }.Select({{lambdaRuntimeAsyncAttribute}}{{lambda}})); + } + } + """; + + var comp = CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition], options: TestOptions.ReleaseDll, parseOptions: TestOptions.RegularPreview); + comp.VerifyEmitDiagnostics(); + } + + [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/82551")] + [CombinatorialData] + public void RuntimeAsync_OverloadResolution_AsyncLambdaReturnInference_InferenceScenarios(bool explicitReturnType, bool statementBody, bool runtimeAsyncEnabledOnLambda) + { + var lambda = (explicitReturnType, statementBody) switch + { + (true, true) => "async Task (string x) => { return await Task.FromResult(x.Length); }", + (true, false) => "async Task (string x) => await Task.FromResult(x.Length)", + (false, true) => "async (x) => { return await Task.FromResult(x.Length); }", + (false, false) => "async (x) => await Task.FromResult(x.Length)", + }; + var lambdaRuntimeAsyncAttribute = $"[System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute({(runtimeAsyncEnabledOnLambda ? "true" : "false")})] "; + + var source = $$""" + using System; + using System.Threading.Tasks; + + class Program + { + static Task Use(Func> f) => Task.CompletedTask; + static Task Use(Func> f) => Task.CompletedTask; + + static async Task Main() + { + await Use({{lambdaRuntimeAsyncAttribute}}{{lambda}}); + } + } + """; + + var comp = CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); + comp.VerifyEmitDiagnostics(); + } + + [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/82551")] + [CombinatorialData] + public void RuntimeAsync_OverloadResolution_AsyncLambdaReturnInference_GenericCandidate_InferenceScenarios(bool explicitReturnType, bool statementBody, bool runtimeAsyncEnabledOnLambda) + { + var lambda = (explicitReturnType, statementBody) switch + { + (true, true) => "async Task (string x) => { return await Task.FromResult(x.Length); }", + (true, false) => "async Task (string x) => await Task.FromResult(x.Length)", + (false, true) => "async (x) => { return await Task.FromResult(x.Length); }", + (false, false) => "async (x) => await Task.FromResult(x.Length)", + }; + var lambdaRuntimeAsyncAttribute = $"[System.Runtime.CompilerServices.RuntimeAsyncMethodGenerationAttribute({(runtimeAsyncEnabledOnLambda ? "true" : "false")})] "; + + var source = $$""" + using System; + using System.Threading.Tasks; + + class Program + { + static Task Use(Func> f) => Task.CompletedTask; + static Task Use(Func> f) => Task.CompletedTask; + + static async Task Main() + { + await Use({{lambdaRuntimeAsyncAttribute}}{{lambda}}); + } + } + """; + + var comp = CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition], options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); + comp.VerifyEmitDiagnostics(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82551")] + public void RuntimeAsync_OverloadResolution_AsyncLambdaReturnInference_ValueTaskTarget() + { + var source = """ + using System; + using System.Threading.Tasks; + + class Program + { + static Task Use(Func> f) => Task.CompletedTask; + static Task Use(Func> f) => Task.CompletedTask; + + static async Task Main() + { + await Use(async ValueTask (int x) => await Task.FromResult(x)); + } + } + """; + + var comp = CreateRuntimeAsyncCompilation(source, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); + comp.VerifyEmitDiagnostics(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82551")] + public void RuntimeAsync_OverloadResolution_AsyncLambdaReturnInference_AmbiguousTaskLike_NoCrash() + { + var source = """ + using System; + using System.Threading.Tasks; + + class Program + { + static void Use(Func> f) { } + static void Use(Func> f) { } + + static void Main() + { + Use(async x => await Task.FromResult(x)); + } + } + """; + + var comp = CreateRuntimeAsyncCompilation(source, options: TestOptions.ReleaseExe, parseOptions: TestOptions.RegularPreview); + + comp.VerifyEmitDiagnostics( + // (11,9): error CS0121: The call is ambiguous between the following methods or properties: 'Program.Use(System.Func>)' and 'Program.Use(System.Func>)' + // Use(async x => await Task.FromResult(x)); + Diagnostic(ErrorCode.ERR_AmbigCall, "Use").WithArguments("Program.Use(System.Func>)", "Program.Use(System.Func>)").WithLocation(11, 9) + ); + } + [Theory] [CombinatorialData] public void RuntimeAsync_CompilerFeatureFlag_EnabledWithoutRuntimeAsync(bool withNonCoreLibSources) @@ -9150,6 +9554,7 @@ .locals init (int V_0, //result IL_0001: newobj "C..ctor()" IL_0006: call "C.Awaiter C.GetAwaiter()" IL_000b: stloc.s V_4 + // sequence point: IL_000d: ldloc.s V_4 IL_000f: callvirt "bool C.Awaiter.IsCompleted.get" IL_0014: brtrue.s IL_001e @@ -9174,6 +9579,7 @@ .locals init (int V_0, //result IL_0033: newobj "C..ctor()" IL_0038: call "C.Awaiter C.GetAwaiter()" IL_003d: stloc.s V_6 + // sequence point: IL_003f: ldloc.s V_6 IL_0041: callvirt "bool C.Awaiter.IsCompleted.get" IL_0046: brtrue.s IL_0050 @@ -9202,6 +9608,7 @@ .locals init (int V_0, //result IL_006a: ldloc.0 IL_006b: call "void System.Console.Write(int)" IL_0070: nop + // sequence point: } IL_0071: ret } """, sequencePointDisplay: SequencePointDisplayMode.Enhanced); @@ -10636,5 +11043,293 @@ .locals init (Program.<
$>d__0 V_0) } """); } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82397")] + public void RuntimeAsyncLocalFunctionAwaitedFromNonRuntimeAsyncLambda() + { + var source = """ +using System.Threading.Tasks; + +await Task.Run( +[System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)] +async () => +{ + await Func(); + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] + static async Task Func() + { + await Task.Delay(1); + await Task.Yield(); + } +}); +"""; + + var comp = CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition]); + var verifier = CompileAndVerify(comp, verify: Verification.Fails with + { + ILVerifyMessage = $""" + {ReturnValueMissing("
$", "0x29")} + {ReturnValueMissing("<
$>g__Func|0_1", "0x2f")} + """ + + }); + + verifier.VerifyDiagnostics(); + + verifier.VerifyIL("Program.<
$>g__Func|0_1", """ +{ + // Code size 48 (0x30) + .maxstack 1 + .locals init (System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter V_0, + System.Runtime.CompilerServices.YieldAwaitable V_1) + IL_0000: ldc.i4.1 + IL_0001: call "System.Threading.Tasks.Task System.Threading.Tasks.Task.Delay(int)" + IL_0006: call "void System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.Task)" + IL_000b: call "System.Runtime.CompilerServices.YieldAwaitable System.Threading.Tasks.Task.Yield()" + IL_0010: stloc.1 + IL_0011: ldloca.s V_1 + IL_0013: call "System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter System.Runtime.CompilerServices.YieldAwaitable.GetAwaiter()" + IL_0018: stloc.0 + IL_0019: ldloca.s V_0 + IL_001b: call "bool System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter.IsCompleted.get" + IL_0020: brtrue.s IL_0028 + IL_0022: ldloc.0 + IL_0023: call "void System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiter(System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter)" + IL_0028: ldloca.s V_0 + IL_002a: call "void System.Runtime.CompilerServices.YieldAwaitable.YieldAwaiter.GetResult()" + IL_002f: ret +} +"""); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82397")] + public void NonRuntimeAsyncLocalFunctionAwaitedFromRuntimeAsyncLambda() + { + var source = """ +using System.Threading.Tasks; + +await Task.Run( +[System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(true)] +async () => +{ + await Func(); + + [System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)] + static async Task Func() + { + await Task.Delay(1); + await Task.Yield(); + } +}); +"""; + + var comp = CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition]); + var verifier = CompileAndVerify(comp, verify: Verification.Fails with + { + ILVerifyMessage = $""" + {ReturnValueMissing("
$", "0x29")} + {ReturnValueMissing("<
$>b__0_0", "0xa")} + """ + + }); + + verifier.VerifyDiagnostics(); + + verifier.VerifyIL("Program.<>c.<
$>b__0_0()", """ +{ + // Code size 11 (0xb) + .maxstack 1 + IL_0000: call "System.Threading.Tasks.Task Program.<
$>g__Func|0_1()" + IL_0005: call "void System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.Task)" + IL_000a: ret +} +"""); + } + + [Fact] + public void RuntimeAsyncNullable_WithTaskReturningMethod() + { + var source = """ +using System.Threading.Tasks; + +#nullable enable + +await M(null); + +async Task? M(object o) { } +"""; + + var comp = CreateRuntimeAsyncCompilation(source); + var verifier = CompileAndVerify(comp, verify: Verification.Fails with + { + ILVerifyMessage = $""" + {ReturnValueMissing("
$", "0xb")} + {ReturnValueMissing("<
$>g__M|0_0", "0x0")} + """ + }); + verifier.VerifyDiagnostics( + // (5,7): warning CS8604: Possible null reference argument for parameter 'task' in 'void AsyncHelpers.Await(Task task)'. + // await M(null); + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "M(null)").WithArguments("task", "void AsyncHelpers.Await(Task task)").WithLocation(5, 7), + // (5,9): warning CS8625: Cannot convert null literal to non-nullable reference type. + // await M(null); + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(5, 9) + ); + } + + [Fact] + public void RuntimeAsyncNullable_WithTaskLikeReturningMethod() + { + var corlib = """ + namespace System + { + public class Attribute {} + public enum AttributeTargets {} + public class AttributeUsageAttribute : Attribute + { + public AttributeUsageAttribute(AttributeTargets validOn) {} + public bool AllowMultiple { get; set; } + public bool Inherited { get; set; } + } + public struct Boolean {} + public struct Byte {} + public abstract class Enum {} + public class Exception {} + public struct Int32 {} + public class Object {} + public class String {} + public class ValueType {} + public class Void {} + + namespace Threading.Tasks + { + public class Task + { + public Runtime.CompilerServices.TaskAwaiter GetAwaiter() => throw null!; + public static Task CompletedTask => throw null!; + } + } + + namespace Runtime.CompilerServices + { + public interface INotifyCompletion {} + public interface ICriticalNotifyCompletion : INotifyCompletion {} + public class TaskAwaiter : ICriticalNotifyCompletion + { + public bool IsCompleted => false; + public void GetResult() {} + } + + public static class AsyncHelpers + { + public static void UnsafeAwaitAwaiter(TAwaiter awaiter) where TAwaiter : notnull, INotifyCompletion {} + } + } + } + """; + + var source = """ +#nullable enable + +await M(null); + +MyTask M(object o) => throw null!; + +public class MyTask +{ + public System.Runtime.CompilerServices.TaskAwaiter? GetAwaiter() => throw null!; +} +"""; + + var comp = CreateEmptyCompilation([source, corlib], parseOptions: WithRuntimeAsync(TestOptions.RegularPreview), assemblyName: "TestAssembly"); + var verifier = CompileAndVerify(comp, verify: Verification.Skipped); + verifier.VerifyDiagnostics( + // warning CS8021: No value for RuntimeMetadataVersion found. No assembly containing System.Object was found nor was a value for RuntimeMetadataVersion specified through options. + Diagnostic(ErrorCode.WRN_NoRuntimeMetadataVersion).WithLocation(1, 1), + // (3,7): warning CS8714: The type 'System.Runtime.CompilerServices.TaskAwaiter?' cannot be used as type parameter 'TAwaiter' in the generic type or method 'AsyncHelpers.UnsafeAwaitAwaiter(TAwaiter)'. Nullability of type argument 'System.Runtime.CompilerServices.TaskAwaiter?' doesn't match 'notnull' constraint. + // await M(null); + Diagnostic(ErrorCode.WRN_NullabilityMismatchInTypeParameterNotNullConstraint, "M").WithArguments("System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiter(TAwaiter)", "TAwaiter", "System.Runtime.CompilerServices.TaskAwaiter?").WithLocation(3, 7), + // (3,9): warning CS8625: Cannot convert null literal to non-nullable reference type. + // await M(null); + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(3, 9) + ); + } + + [Theory] + [InlineData("Task")] + [InlineData("ValueTask")] + public void RuntimeAsyncAwaitNullableFlow_WithTaskReturningMethod(string taskType) + { + var source = $$""" + using System.Threading.Tasks; + + #nullable enable + + _ = (await GetNullableResultAsync()).ToString(); + _ = (await GetNullableResultAsync().ConfigureAwait(true)).ToString(); + var x = await GetNullableResultAsync(); + _ = x.ToString(); // 1 + x = await GetNullableResultAsync().ConfigureAwait(false); + _ = x.ToString(); // 2 + + {{taskType}} GetNullableResultAsync() => default!; + """; + + var comp = CreateRuntimeAsyncCompilation(source); + comp.VerifyDiagnostics( + // (5,6): warning CS8602: Dereference of a possibly null reference. + // _ = (await GetNullableResultAsync()).ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "await GetNullableResultAsync()").WithLocation(5, 6), + // (6,6): warning CS8602: Dereference of a possibly null reference. + // _ = (await GetNullableResultAsync().ConfigureAwait(true)).ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "await GetNullableResultAsync().ConfigureAwait(true)").WithLocation(6, 6), + // (8,5): warning CS8602: Dereference of a possibly null reference. + // _ = x.ToString(); // 1 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "x").WithLocation(8, 5), + // (10,5): warning CS8602: Dereference of a possibly null reference. + // _ = x.ToString(); // 2 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "x").WithLocation(10, 5) + ); + } + + [Fact] + public void RuntimeAsyncAwaitNullableFlow_WithTaskLikeReturningMethod() + { + var source = """ + #nullable enable + + _ = (await GetNullableResultTaskLike()).ToString(); + var x = await GetNullableResultTaskLike(); + _ = x.ToString(); + + MyTask GetNullableResultTaskLike() => throw null!; + + public class MyTask + { + public MyAwaiter GetAwaiter() => throw null!; + } + + public class MyAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion + { + public bool IsCompleted => false; + public string? GetResult() => null; + public void OnCompleted(System.Action continuation) { } + public void UnsafeOnCompleted(System.Action continuation) { } + } + """; + + var comp = CreateRuntimeAsyncCompilation(source); + comp.VerifyDiagnostics( + // (3,6): warning CS8602: Dereference of a possibly null reference. + // _ = (await GetNullableResultTaskLike()).ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "await GetNullableResultTaskLike()").WithLocation(3, 6), + // (5,5): warning CS8602: Dereference of a possibly null reference. + // _ = x.ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "x").WithLocation(5, 5) + ); + } } } diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAwaitForeachTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAwaitForeachTests.cs index 85cf96b67339..cb16d88ed5a2 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAwaitForeachTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAwaitForeachTests.cs @@ -9655,6 +9655,80 @@ .locals init (C.Enumerator V_0, """); } + [Fact] + public void RuntimeAsync_PatternBasedDisposal_ReturnsNullableTask_Warns() + { + string source = @"#nullable enable +using System.Threading.Tasks; +class C +{ + static async Task M() + { + await foreach (var i in new C()) + { + } + } + public Enumerator GetAsyncEnumerator() => new Enumerator(); + public sealed class Enumerator + { + public int Current => 0; + public Task MoveNextAsync() => Task.FromResult(false); + public Task? DisposeAsync() => null; + } +}"; + var comp = CreateRuntimeAsyncCompilation(source); + comp.VerifyEmitDiagnostics( + // (7,33): warning CS8604: Possible null reference argument for parameter 'task' in 'void AsyncHelpers.Await(Task task)'. + // await foreach (var i in new C()) + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "new C()").WithArguments("task", "void AsyncHelpers.Await(Task task)").WithLocation(7, 33) + ); + } + + [Fact] + public void RuntimeAsync_UserDefinedIAsyncDisposable_ReturnsNullableValueTask_Errors() + { + string source = @"#nullable enable +using System.Threading.Tasks; +namespace System +{ + public interface IAsyncDisposable + { + ValueTask? DisposeAsync(); + } +} +class C +{ + static async Task M() + { + await foreach (var i in new C()) + { + } + } + public Enumerator GetAsyncEnumerator() => new Enumerator(); + public sealed class Enumerator : System.IAsyncDisposable + { + public int Current => 0; + public Task MoveNextAsync() => Task.FromResult(false); + ValueTask? System.IAsyncDisposable.DisposeAsync() => null; + } +}"; + var comp = CreateRuntimeAsyncCompilation(source); + comp.VerifyEmitDiagnostics( + // (14,9): error CS0656: Missing compiler required member 'System.IAsyncDisposable.DisposeAsync' + // await foreach (var i in new C()) + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, @"await foreach (var i in new C()) + { + }").WithArguments("System.IAsyncDisposable", "DisposeAsync").WithLocation(14, 9), + // (19,45): warning CS0436: The type 'IAsyncDisposable' in '' conflicts with the imported type 'IAsyncDisposable' in 'System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. Using the type defined in ''. + // public sealed class Enumerator : System.IAsyncDisposable + Diagnostic(ErrorCode.WRN_SameFullNameThisAggAgg, "IAsyncDisposable").WithArguments("", "System.IAsyncDisposable", "System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.IAsyncDisposable").WithLocation(19, 45), + // (23,27): warning CS0436: The type 'IAsyncDisposable' in '' conflicts with the imported type 'IAsyncDisposable' in 'System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. Using the type defined in ''. + // ValueTask? System.IAsyncDisposable.DisposeAsync() => null; + Diagnostic(ErrorCode.WRN_SameFullNameThisAggAgg, "IAsyncDisposable").WithArguments("", "System.IAsyncDisposable", "System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.IAsyncDisposable").WithLocation(23, 27) + + ); + } + [Fact] public void PatternBasedDisposal_WithOptionalParameter() { diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAwaitUsingTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAwaitUsingTests.cs index 3abd61bb4beb..4ffb3eea72b3 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAwaitUsingTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenAwaitUsingTests.cs @@ -4769,6 +4769,62 @@ .locals init (C V_0, //x """); } + [Fact] + public void RuntimeAsync_TestPatternBasedDisposal_ReturnsNullableTask_Warns() + { + string source = @"#nullable enable +using System.Threading.Tasks; +class C +{ + static async Task M() + { + await using var x = new C(); + } + public Task? DisposeAsync() => null; +}"; + var comp = CreateRuntimeAsyncCompilation(source); + comp.VerifyEmitDiagnostics( + // (7,9): warning CS8604: Possible null reference argument for parameter 'task' in 'void AsyncHelpers.Await(Task task)'. + // await using var x = new C(); + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "await using var x = new C();").WithArguments("task", "void AsyncHelpers.Await(Task task)").WithLocation(7, 9) + ); + } + + [Fact] + public void RuntimeAsync_TestUserDefinedIAsyncDisposable_ReturnsNullableValueTask() + { + string source = @"#nullable enable +using System.Threading.Tasks; +namespace System +{ + public interface IAsyncDisposable + { + ValueTask? DisposeAsync(); + } +} +class C : System.IAsyncDisposable +{ + static async Task M() + { + await using var x = new C(); + } + + ValueTask? System.IAsyncDisposable.DisposeAsync() => null; +}"; + var comp = CreateRuntimeAsyncCompilation(source); + comp.VerifyEmitDiagnostics( + // (10,18): warning CS0436: The type 'IAsyncDisposable' in '' conflicts with the imported type 'IAsyncDisposable' in 'System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. Using the type defined in ''. + // class C : System.IAsyncDisposable + Diagnostic(ErrorCode.WRN_SameFullNameThisAggAgg, "IAsyncDisposable").WithArguments("", "System.IAsyncDisposable", "System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.IAsyncDisposable").WithLocation(10, 18), + // (14,9): error CS0656: Missing compiler required member 'System.IAsyncDisposable.DisposeAsync' + // await using var x = new C(); + Diagnostic(ErrorCode.ERR_MissingPredefinedMember, "await").WithArguments("System.IAsyncDisposable", "DisposeAsync").WithLocation(14, 9), + // (17,23): warning CS0436: The type 'IAsyncDisposable' in '' conflicts with the imported type 'IAsyncDisposable' in 'System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. Using the type defined in ''. + // ValueTask? System.IAsyncDisposable.DisposeAsync() => null; + Diagnostic(ErrorCode.WRN_SameFullNameThisAggAgg, "IAsyncDisposable").WithArguments("", "System.IAsyncDisposable", "System.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", "System.IAsyncDisposable").WithLocation(17, 23) + ); + } + [Fact] public void TestInRegularMethod() { diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenReadonlyStructTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenReadonlyStructTests.cs index a1dc3eaf6f64..8c8892d4fefe 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenReadonlyStructTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenReadonlyStructTests.cs @@ -512,29 +512,53 @@ public void Test() var comp = CompileAndVerify(text, parseOptions: TestOptions.Regular, verify: Verification.Passes, expectedOutput: @"Program+S1Program+S1"); - // We may be able to optimize this case (ie. avoid defensive copy) since the struct and the caller are in the same module - // Tracked by https://github.com/dotnet/roslyn/issues/66365 comp.VerifyIL("Program.S1.Test()", @" { - // Code size 47 (0x2f) + // Code size 39 (0x27) .maxstack 1 - .locals init (Program.S1 V_0) IL_0000: ldarg.0 IL_0001: ldobj ""Program.S1"" IL_0006: box ""Program.S1"" IL_000b: call ""System.Type object.GetType()"" IL_0010: call ""void System.Console.Write(object)"" IL_0015: ldarg.0 - IL_0016: ldobj ""Program.S1"" - IL_001b: stloc.0 - IL_001c: ldloca.s V_0 - IL_001e: constrained. ""Program.S1"" - IL_0024: callvirt ""string object.ToString()"" - IL_0029: call ""void System.Console.Write(string)"" - IL_002e: ret + IL_0016: constrained. ""Program.S1"" + IL_001c: callvirt ""string object.ToString()"" + IL_0021: call ""void System.Console.Write(string)"" + IL_0026: ret }"); } + [Fact, WorkItem(76288, "https://github.com/dotnet/roslyn/issues/76288")] + public void NoDefensiveCopy_ReadonlyStructField_ConstrainedCall() + { + var text = @" +#pragma warning disable CS0649 // Field is never assigned to +struct X +{ + private Y a; + public readonly string W() => a.ToString(); +} + +readonly struct Y +{ +} +"; + var comp = CompileAndVerify(text, options: TestOptions.ReleaseDll, verify: Verification.Passes); + comp.VerifyDiagnostics(); + comp.VerifyIL("X.W()", @" +{ + // Code size 18 (0x12) + .maxstack 1 + IL_0000: ldarg.0 + IL_0001: ldflda ""Y X.a"" + IL_0006: constrained. ""Y"" + IL_000c: callvirt ""string object.ToString()"" + IL_0011: ret +} +"); + } + [Fact] public void AssignThis() { diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenRefConditionalOperatorTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenRefConditionalOperatorTests.cs index 681dc2e8e993..eba4de751741 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenRefConditionalOperatorTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenRefConditionalOperatorTests.cs @@ -1225,22 +1225,22 @@ public void TestRefOnPointerIndirection_ThroughTernary_01() } "; - verify(TestOptions.UnsafeReleaseExe, Verification.Passes, @" + verify(TestOptions.UnsafeReleaseExe, Verification.Fails, @" { - // Code size 22 (0x16) + // Code size 21 (0x15) .maxstack 1 + .locals init (int& V_0) //x IL_0000: ldc.i4.1 - IL_0001: brtrue.s IL_0008 + IL_0001: brtrue.s IL_0007 IL_0003: ldc.i4.1 IL_0004: conv.i - IL_0005: pop - IL_0006: br.s IL_000b - IL_0008: ldc.i4.0 - IL_0009: conv.i - IL_000a: pop - IL_000b: ldstr ""run"" - IL_0010: call ""void System.Console.WriteLine(string)"" - IL_0015: ret + IL_0005: br.s IL_0009 + IL_0007: ldc.i4.0 + IL_0008: conv.i + IL_0009: stloc.0 + IL_000a: ldstr ""run"" + IL_000f: call ""void System.Console.WriteLine(string)"" + IL_0014: ret } "); @@ -1302,24 +1302,27 @@ public void TestRefOnPointerIndirection_ThroughTernary_02() verify(TestOptions.UnsafeReleaseExe, @" { - // Code size 28 (0x1c) + // Code size 32 (0x20) .maxstack 1 .locals init (int V_0, //i1 - int* V_1) //p1 + int* V_1, //p1 + int& V_2) //x IL_0000: ldc.i4.0 IL_0001: stloc.0 IL_0002: ldloca.s V_0 IL_0004: conv.u IL_0005: stloc.1 IL_0006: ldc.i4.1 - IL_0007: brfalse.s IL_0011 - IL_0009: ldloc.1 - IL_000a: ldind.i4 - IL_000b: call ""int* Program.<
$>g__M|0_0(int)"" - IL_0010: pop - IL_0011: ldstr ""run"" - IL_0016: call ""void System.Console.WriteLine(string)"" - IL_001b: ret + IL_0007: brtrue.s IL_000d + IL_0009: ldloca.s V_0 + IL_000b: br.s IL_0014 + IL_000d: ldloc.1 + IL_000e: ldind.i4 + IL_000f: call ""int* Program.<
$>g__M|0_0(int)"" + IL_0014: stloc.2 + IL_0015: ldstr ""run"" + IL_001a: call ""void System.Console.WriteLine(string)"" + IL_001f: ret } "); diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenRefLocalTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenRefLocalTests.cs index 7c0e6b10362d..bbcda63d5772 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenRefLocalTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/CodeGenRefLocalTests.cs @@ -4344,20 +4344,23 @@ public void TestRefOnPointerIndirection_03() verify(TestOptions.UnsafeReleaseExe, Verification.Fails, @" { - // Code size 19 (0x13) - .maxstack 1 + // Code size 21 (0x15) + .maxstack 2 .locals init (int V_0, //i1 - int& V_1) //i2 + int& V_1, //i2 + int& V_2) //i3 IL_0000: ldc.i4.0 IL_0001: stloc.0 IL_0002: ldloca.s V_0 IL_0004: stloc.1 IL_0005: ldc.i4.0 IL_0006: conv.i - IL_0007: stloc.1 - IL_0008: ldstr ""run"" - IL_000d: call ""void System.Console.WriteLine(string)"" - IL_0012: ret + IL_0007: dup + IL_0008: stloc.1 + IL_0009: stloc.2 + IL_000a: ldstr ""run"" + IL_000f: call ""void System.Console.WriteLine(string)"" + IL_0014: ret } "); diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/ITuplePatternTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/ITuplePatternTests.cs index b899bd8edfe1..267123e41392 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/ITuplePatternTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/ITuplePatternTests.cs @@ -228,69 +228,69 @@ public string M() "; var verifier = CompileAndVerify(CreatePatternCompilation(source, TestOptions.DebugDll)); verifier.VerifyIL("C.M", @" - { - // Code size 110 (0x6e) - .maxstack 2 - .locals init (C V_0, - int V_1, - object V_2, - int V_3, - object V_4, - int V_5, - C V_6, - string V_7) - IL_0000: nop - IL_0001: ldarg.0 - IL_0002: stloc.s V_6 - IL_0004: ldloc.s V_6 - IL_0006: stloc.0 - IL_0007: ldloc.0 - IL_0008: isinst ""System.Runtime.CompilerServices.ITuple"" - IL_000d: brfalse.s IL_0061 - IL_000f: ldloc.0 - IL_0010: callvirt ""int System.Runtime.CompilerServices.ITuple.Length.get"" - IL_0015: stloc.1 - IL_0016: ldloc.1 - IL_0017: ldc.i4.2 - IL_0018: bne.un.s IL_0061 - IL_001a: ldloc.0 - IL_001b: ldc.i4.0 - IL_001c: callvirt ""object System.Runtime.CompilerServices.ITuple.this[int].get"" - IL_0021: stloc.2 - IL_0022: ldloc.2 - IL_0023: isinst ""int"" - IL_0028: brfalse.s IL_0061 - IL_002a: ldloc.2 - IL_002b: unbox.any ""int"" - IL_0030: stloc.3 - IL_0031: ldloc.3 - IL_0032: ldc.i4.1 - IL_0033: bne.un.s IL_0061 - IL_0035: ldloc.0 - IL_0036: ldc.i4.1 - IL_0037: callvirt ""object System.Runtime.CompilerServices.ITuple.this[int].get"" - IL_003c: stloc.s V_4 - IL_003e: ldloc.s V_4 - IL_0040: isinst ""int"" - IL_0045: brfalse.s IL_0061 - IL_0047: ldloc.s V_4 - IL_0049: unbox.any ""int"" - IL_004e: stloc.s V_5 - IL_0050: ldloc.s V_5 - IL_0052: ldc.i4.1 - IL_0053: beq.s IL_0057 - IL_0055: br.s IL_0061 - IL_0057: ldarg.0 - IL_0058: call ""string C.Value.get"" - IL_005d: stloc.s V_7 - IL_005f: br.s IL_006b - IL_0061: ldarg.0 - IL_0062: call ""string C.Value.get"" - IL_0067: stloc.s V_7 - IL_0069: br.s IL_006b - IL_006b: ldloc.s V_7 - IL_006d: ret - }"); +{ + // Code size 105 (0x69) + .maxstack 2 + .locals init (C V_0, + int V_1, + object V_2, + int V_3, + object V_4, + int V_5, + C V_6, + string V_7) + IL_0000: nop + IL_0001: ldarg.0 + IL_0002: stloc.s V_6 + IL_0004: ldloc.s V_6 + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: brfalse.s IL_005c + IL_000a: ldloc.0 + IL_000b: callvirt ""int System.Runtime.CompilerServices.ITuple.Length.get"" + IL_0010: stloc.1 + IL_0011: ldloc.1 + IL_0012: ldc.i4.2 + IL_0013: bne.un.s IL_005c + IL_0015: ldloc.0 + IL_0016: ldc.i4.0 + IL_0017: callvirt ""object System.Runtime.CompilerServices.ITuple.this[int].get"" + IL_001c: stloc.2 + IL_001d: ldloc.2 + IL_001e: isinst ""int"" + IL_0023: brfalse.s IL_005c + IL_0025: ldloc.2 + IL_0026: unbox.any ""int"" + IL_002b: stloc.3 + IL_002c: ldloc.3 + IL_002d: ldc.i4.1 + IL_002e: bne.un.s IL_005c + IL_0030: ldloc.0 + IL_0031: ldc.i4.1 + IL_0032: callvirt ""object System.Runtime.CompilerServices.ITuple.this[int].get"" + IL_0037: stloc.s V_4 + IL_0039: ldloc.s V_4 + IL_003b: isinst ""int"" + IL_0040: brfalse.s IL_005c + IL_0042: ldloc.s V_4 + IL_0044: unbox.any ""int"" + IL_0049: stloc.s V_5 + IL_004b: ldloc.s V_5 + IL_004d: ldc.i4.1 + IL_004e: beq.s IL_0052 + IL_0050: br.s IL_005c + IL_0052: ldarg.0 + IL_0053: call ""string C.Value.get"" + IL_0058: stloc.s V_7 + IL_005a: br.s IL_0066 + IL_005c: ldarg.0 + IL_005d: call ""string C.Value.get"" + IL_0062: stloc.s V_7 + IL_0064: br.s IL_0066 + IL_0066: ldloc.s V_7 + IL_0068: ret +} +"); } } diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/IndexAndRangeTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/IndexAndRangeTests.cs index e0ede9af8f89..22544b3db8a3 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/IndexAndRangeTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/IndexAndRangeTests.cs @@ -17,6 +17,11 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.CodeGen { public class IndexAndRangeTests : CSharpTestBase { + private static string ExpectedOutput(string output) + { + return ExecutionConditionUtil.IsMonoOrCoreClr ? output : null; + } + private CompilationVerifier CompileAndVerifyWithIndexAndRange(string s, string expectedOutput = null) { var comp = CreateCompilationWithIndexAndRange( @@ -826,55 +831,58 @@ public static void Main() } } }"; + // Without ReadOnlySpan.Slice var comp = CreateCompilationWithIndexAndRangeAndSpanAndMemoryExtensions(src, TestOptions.ReleaseExe); + comp.MakeMemberMissing(SpecialMember.System_String__SubstringInt); + comp.MakeMemberMissing(WellKnownMember.System_ReadOnlySpan_T__Slice_Int); var verifier = CompileAndVerify(comp, expectedOutput: @" abcd abcd"); verifier.VerifyIL("C.Main", @" { // Code size 84 (0x54) - .maxstack 4 - .locals init (System.ReadOnlySpan V_0, //span - string V_1, - System.ReadOnlySpan V_2, - int V_3, - System.ReadOnlySpan& V_4) - IL_0000: ldstr ""abcd"" - IL_0005: dup - IL_0006: stloc.1 - IL_0007: ldloc.1 - IL_0008: ldc.i4.0 - IL_0009: ldloc.1 - IL_000a: callvirt ""int string.Length.get"" - IL_000f: callvirt ""string string.Substring(int, int)"" - IL_0014: call ""void System.Console.WriteLine(string)"" - IL_0019: call ""System.ReadOnlySpan System.MemoryExtensions.AsSpan(string)"" - IL_001e: stloc.0 - IL_001f: ldloca.s V_0 - IL_0021: stloc.s V_4 - IL_0023: ldloc.s V_4 - IL_0025: ldc.i4.0 - IL_0026: ldloc.s V_4 - IL_0028: call ""int System.ReadOnlySpan.Length.get"" - IL_002d: call ""System.ReadOnlySpan System.ReadOnlySpan.Slice(int, int)"" - IL_0032: stloc.2 - IL_0033: ldc.i4.0 - IL_0034: stloc.3 - IL_0035: br.s IL_0049 - IL_0037: ldloca.s V_2 - IL_0039: ldloc.3 - IL_003a: call ""ref readonly char System.ReadOnlySpan.this[int].get"" - IL_003f: ldind.u2 - IL_0040: call ""void System.Console.Write(char)"" - IL_0045: ldloc.3 - IL_0046: ldc.i4.1 - IL_0047: add - IL_0048: stloc.3 - IL_0049: ldloc.3 - IL_004a: ldloca.s V_2 - IL_004c: call ""int System.ReadOnlySpan.Length.get"" - IL_0051: blt.s IL_0037 - IL_0053: ret + .maxstack 4 + .locals init (System.ReadOnlySpan V_0, //span + string V_1, + System.ReadOnlySpan V_2, + int V_3, + System.ReadOnlySpan& V_4) + IL_0000: ldstr ""abcd"" + IL_0005: dup + IL_0006: stloc.1 + IL_0007: ldloc.1 + IL_0008: ldc.i4.0 + IL_0009: ldloc.1 + IL_000a: callvirt ""int string.Length.get"" + IL_000f: callvirt ""string string.Substring(int, int)"" + IL_0014: call ""void System.Console.WriteLine(string)"" + IL_0019: call ""System.ReadOnlySpan System.MemoryExtensions.AsSpan(string)"" + IL_001e: stloc.0 + IL_001f: ldloca.s V_0 + IL_0021: stloc.s V_4 + IL_0023: ldloc.s V_4 + IL_0025: ldc.i4.0 + IL_0026: ldloc.s V_4 + IL_0028: call ""int System.ReadOnlySpan.Length.get"" + IL_002d: call ""System.ReadOnlySpan System.ReadOnlySpan.Slice(int, int)"" + IL_0032: stloc.2 + IL_0033: ldc.i4.0 + IL_0034: stloc.3 + IL_0035: br.s IL_0049 + IL_0037: ldloca.s V_2 + IL_0039: ldloc.3 + IL_003a: call ""ref readonly char System.ReadOnlySpan.this[int].get"" + IL_003f: ldind.u2 + IL_0040: call ""void System.Console.Write(char)"" + IL_0045: ldloc.3 + IL_0046: ldc.i4.1 + IL_0047: add + IL_0048: stloc.3 + IL_0049: ldloc.3 + IL_004a: ldloca.s V_2 + IL_004c: call ""int System.ReadOnlySpan.Length.get"" + IL_0051: blt.s IL_0037 + IL_0053: ret }"); var (model, elementAccesses) = GetModelAndAccesses(comp); @@ -886,6 +894,58 @@ .locals init (System.ReadOnlySpan V_0, //span VerifyIndexCall(substringCall, "Substring", "String"); VerifyIndexCall(sliceCall, "Slice", "ReadOnlySpan"); + + // With ReadOnlySpan.Slice + comp = CreateCompilationWithIndexAndRangeAndSpanAndMemoryExtensions(src, TestOptions.ReleaseExe); + verifier = CompileAndVerify(comp, expectedOutput: @" +abcd +abcd"); + verifier.VerifyIL("C.Main", @" +{ + // Code size 65 (0x41) + .maxstack 3 + .locals init (System.ReadOnlySpan V_0, //span + System.ReadOnlySpan V_1, + int V_2) + IL_0000: ldstr ""abcd"" + IL_0005: dup + IL_0006: ldc.i4.0 + IL_0007: callvirt ""string string.Substring(int)"" + IL_000c: call ""void System.Console.WriteLine(string)"" + IL_0011: call ""System.ReadOnlySpan System.MemoryExtensions.AsSpan(string)"" + IL_0016: stloc.0 + IL_0017: ldloca.s V_0 + IL_0019: ldc.i4.0 + IL_001a: call ""System.ReadOnlySpan System.ReadOnlySpan.Slice(int)"" + IL_001f: stloc.1 + IL_0020: ldc.i4.0 + IL_0021: stloc.2 + IL_0022: br.s IL_0036 + IL_0024: ldloca.s V_1 + IL_0026: ldloc.2 + IL_0027: call ""ref readonly char System.ReadOnlySpan.this[int].get"" + IL_002c: ldind.u2 + IL_002d: call ""void System.Console.Write(char)"" + IL_0032: ldloc.2 + IL_0033: ldc.i4.1 + IL_0034: add + IL_0035: stloc.2 + IL_0036: ldloc.2 + IL_0037: ldloca.s V_1 + IL_0039: call ""int System.ReadOnlySpan.Length.get"" + IL_003e: blt.s IL_0024 + IL_0040: ret +}"); + + (model, elementAccesses) = GetModelAndAccesses(comp); + + info = model.GetSymbolInfo(elementAccesses[0]); + substringCall = (IMethodSymbol)info.Symbol; + info = model.GetSymbolInfo(elementAccesses[1]); + sliceCall = (IMethodSymbol)info.Symbol; + + VerifyIndexCall(substringCall, "Substring", "String"); + VerifyIndexCall(sliceCall, "Slice", "ReadOnlySpan"); } [Fact] @@ -1095,7 +1155,8 @@ static void Main() Console.WriteLine(s[0]); Console.WriteLine(s[1]); } -}", TestOptions.ReleaseExe); ; +}", TestOptions.ReleaseExe); + comp.MakeMemberMissing(WellKnownMember.System_ReadOnlySpan_T__Slice_Int); var verifier = CompileAndVerify(comp, expectedOutput: @"f g f @@ -1181,6 +1242,7 @@ static void Main() Console.WriteLine(s[1]); } }", TestOptions.ReleaseExe); + comp.MakeMemberMissing(WellKnownMember.System_Span_T__Slice_Int); var verifier = CompileAndVerify(comp, expectedOutput: @"5 6 5 @@ -1188,66 +1250,66 @@ static void Main() verifier.VerifyIL("C.Main", @" { // Code size 136 (0x88) - .maxstack 4 - .locals init (System.Span V_0, //s - System.Index V_1, //index - System.Span& V_2, - int V_3, - int V_4) - IL_0000: ldc.i4.4 - IL_0001: newarr ""int"" - IL_0006: dup - IL_0007: ldtoken "".__StaticArrayInitTypeSize=16 .B35A10C764778866E34111165FC69660C6171DF0CB0141E39FA0217EF7A97646"" - IL_000c: call ""void System.Runtime.CompilerServices.RuntimeHelpers.InitializeArray(System.Array, System.RuntimeFieldHandle)"" - IL_0011: call ""System.Span System.Span.op_Implicit(int[])"" - IL_0016: stloc.0 - IL_0017: ldloca.s V_0 - IL_0019: dup - IL_001a: call ""int System.Span.Length.get"" - IL_001f: ldc.i4.2 - IL_0020: sub - IL_0021: call ""ref int System.Span.this[int].get"" - IL_0026: ldind.i4 - IL_0027: call ""void System.Console.WriteLine(int)"" - IL_002c: ldloca.s V_1 - IL_002e: ldc.i4.1 - IL_002f: ldc.i4.1 - IL_0030: call ""System.Index..ctor(int, bool)"" - IL_0035: ldloca.s V_0 - IL_0037: stloc.2 - IL_0038: ldloc.2 - IL_0039: ldloca.s V_1 - IL_003b: ldloc.2 - IL_003c: call ""int System.Span.Length.get"" - IL_0041: call ""int System.Index.GetOffset(int)"" - IL_0046: call ""ref int System.Span.this[int].get"" - IL_004b: ldind.i4 - IL_004c: call ""void System.Console.WriteLine(int)"" - IL_0051: ldloca.s V_0 - IL_0053: dup - IL_0054: call ""int System.Span.Length.get"" - IL_0059: stloc.3 - IL_005a: ldloc.3 - IL_005b: ldc.i4.2 - IL_005c: sub - IL_005d: stloc.s V_4 - IL_005f: ldloc.s V_4 - IL_0061: ldloc.3 - IL_0062: ldloc.s V_4 - IL_0064: sub - IL_0065: call ""System.Span System.Span.Slice(int, int)"" - IL_006a: stloc.0 - IL_006b: ldloca.s V_0 - IL_006d: ldc.i4.0 - IL_006e: call ""ref int System.Span.this[int].get"" - IL_0073: ldind.i4 - IL_0074: call ""void System.Console.WriteLine(int)"" - IL_0079: ldloca.s V_0 - IL_007b: ldc.i4.1 - IL_007c: call ""ref int System.Span.this[int].get"" - IL_0081: ldind.i4 - IL_0082: call ""void System.Console.WriteLine(int)"" - IL_0087: ret + .maxstack 4 + .locals init (System.Span V_0, //s + System.Index V_1, //index + System.Span& V_2, + int V_3, + int V_4) + IL_0000: ldc.i4.4 + IL_0001: newarr ""int"" + IL_0006: dup + IL_0007: ldtoken "".__StaticArrayInitTypeSize=16 .B35A10C764778866E34111165FC69660C6171DF0CB0141E39FA0217EF7A97646"" + IL_000c: call ""void System.Runtime.CompilerServices.RuntimeHelpers.InitializeArray(System.Array, System.RuntimeFieldHandle)"" + IL_0011: call ""System.Span System.Span.op_Implicit(int[])"" + IL_0016: stloc.0 + IL_0017: ldloca.s V_0 + IL_0019: dup + IL_001a: call ""int System.Span.Length.get"" + IL_001f: ldc.i4.2 + IL_0020: sub + IL_0021: call ""ref int System.Span.this[int].get"" + IL_0026: ldind.i4 + IL_0027: call ""void System.Console.WriteLine(int)"" + IL_002c: ldloca.s V_1 + IL_002e: ldc.i4.1 + IL_002f: ldc.i4.1 + IL_0030: call ""System.Index..ctor(int, bool)"" + IL_0035: ldloca.s V_0 + IL_0037: stloc.2 + IL_0038: ldloc.2 + IL_0039: ldloca.s V_1 + IL_003b: ldloc.2 + IL_003c: call ""int System.Span.Length.get"" + IL_0041: call ""int System.Index.GetOffset(int)"" + IL_0046: call ""ref int System.Span.this[int].get"" + IL_004b: ldind.i4 + IL_004c: call ""void System.Console.WriteLine(int)"" + IL_0051: ldloca.s V_0 + IL_0053: dup + IL_0054: call ""int System.Span.Length.get"" + IL_0059: stloc.3 + IL_005a: ldloc.3 + IL_005b: ldc.i4.2 + IL_005c: sub + IL_005d: stloc.s V_4 + IL_005f: ldloc.s V_4 + IL_0061: ldloc.3 + IL_0062: ldloc.s V_4 + IL_0064: sub + IL_0065: call ""System.Span System.Span.Slice(int, int)"" + IL_006a: stloc.0 + IL_006b: ldloca.s V_0 + IL_006d: ldc.i4.0 + IL_006e: call ""ref int System.Span.this[int].get"" + IL_0073: ldind.i4 + IL_0074: call ""void System.Console.WriteLine(int)"" + IL_0079: ldloca.s V_0 + IL_007b: ldc.i4.1 + IL_007c: call ""ref int System.Span.this[int].get"" + IL_0081: ldind.i4 + IL_0082: call ""void System.Console.WriteLine(int)"" + IL_0087: ret } "); } @@ -4235,5 +4297,494 @@ static void M(Span s) var comp = CreateCompilation(source, targetFramework: TargetFramework.Net70); comp.VerifyDiagnostics(); } + + [Theory] + [CombinatorialData] + public void SliceStart_01(bool isMissing) + { + // ReadOnlySpan + string source = """ +using System; + +Console.Write(Util.M("0123").ToString()); + +static class Util +{ + public static ReadOnlySpan M(ReadOnlySpan s) => s[1..]; +} +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + if (isMissing) + comp.MakeMemberMissing(WellKnownMember.System_ReadOnlySpan_T__Slice_Int); + + var verify = CompileAndVerify(comp, expectedOutput: ExpectedOutput("123"), verify: Verification.Skipped); + verify.VerifyDiagnostics(); + verify.VerifyIL("Util.M", + isMissing ? """ +{ + // Code size 19 (0x13) + .maxstack 4 + .locals init (System.ReadOnlySpan& V_0) + IL_0000: ldarga.s V_0 + IL_0002: stloc.0 + IL_0003: ldloc.0 + IL_0004: ldc.i4.1 + IL_0005: ldloc.0 + IL_0006: call "int System.ReadOnlySpan.Length.get" + IL_000b: ldc.i4.1 + IL_000c: sub + IL_000d: call "System.ReadOnlySpan System.ReadOnlySpan.Slice(int, int)" + IL_0012: ret +} +""" : """ +{ + // Code size 9 (0x9) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: ldc.i4.1 + IL_0003: call "System.ReadOnlySpan System.ReadOnlySpan.Slice(int)" + IL_0008: ret +} +"""); + } + + [Theory] + [CombinatorialData] + public void SliceStart_02(bool isMissing) + { + // Span + string source = """ +using System; + +Console.Write(Util.M("0123".ToCharArray()).ToString()); + +static class Util +{ + public static Span M(Span s) => s[1..]; +} +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + if (isMissing) + comp.MakeMemberMissing(WellKnownMember.System_Span_T__Slice_Int); + + var verify = CompileAndVerify(comp, expectedOutput: ExpectedOutput("123"), verify: Verification.Skipped); + verify.VerifyDiagnostics(); + verify.VerifyIL("Util.M", + isMissing ? """ +{ + // Code size 19 (0x13) + .maxstack 4 + .locals init (System.Span& V_0) + IL_0000: ldarga.s V_0 + IL_0002: stloc.0 + IL_0003: ldloc.0 + IL_0004: ldc.i4.1 + IL_0005: ldloc.0 + IL_0006: call "int System.Span.Length.get" + IL_000b: ldc.i4.1 + IL_000c: sub + IL_000d: call "System.Span System.Span.Slice(int, int)" + IL_0012: ret +} +""" : """ +{ + // Code size 9 (0x9) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: ldc.i4.1 + IL_0003: call "System.Span System.Span.Slice(int)" + IL_0008: ret +} +"""); + } + + [Theory] + [CombinatorialData] + public void SliceStart_03(bool isMissing) + { + // string + string source = """ +System.Console.Write("0123"[1..]); +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + if (isMissing) + comp.MakeMemberMissing(SpecialMember.System_String__SubstringInt); + + var verify = CompileAndVerify(comp, expectedOutput: ExpectedOutput("123"), verify: Verification.Skipped); + + verify.VerifyDiagnostics(); + verify.VerifyIL("", + isMissing ? """ +{ + // Code size 27 (0x1b) + .maxstack 4 + .locals init (string V_0) + IL_0000: ldstr "0123" + IL_0005: stloc.0 + IL_0006: ldloc.0 + IL_0007: ldc.i4.1 + IL_0008: ldloc.0 + IL_0009: callvirt "int string.Length.get" + IL_000e: ldc.i4.1 + IL_000f: sub + IL_0010: callvirt "string string.Substring(int, int)" + IL_0015: call "void System.Console.Write(string)" + IL_001a: ret +} +""" : """ +{ + // Code size 17 (0x11) + .maxstack 2 + + IL_0000: ldstr "0123" + IL_0005: ldc.i4.1 + IL_0006: callvirt "string string.Substring(int)" + IL_000b: call "void System.Console.Write(string)" + IL_0010: ret +} +"""); + } + + [Theory] + [CombinatorialData] + public void SliceStart_04(bool isMissing) + { + // Memory/ReadOnlyMemory + string source = """ +using System; + +Console.Write(Util.ReadOnly("0123".AsMemory()).ToString()); +Console.Write(Util.Writable("ABCD".ToCharArray().AsMemory()).ToString()); + +static class Util +{ + public static ReadOnlyMemory ReadOnly(ReadOnlyMemory s) => s[1..]; + public static Memory Writable(Memory s) => s[1..]; +} +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + if (isMissing) + { + comp.MakeMemberMissing(WellKnownMember.System_ReadOnlyMemory_T__Slice_Int); + comp.MakeMemberMissing(WellKnownMember.System_Memory_T__Slice_Int); + } + + var verify = CompileAndVerify(comp, expectedOutput: ExpectedOutput("123BCD"), verify: Verification.FailsPEVerify); + + verify.VerifyDiagnostics(); + verify.VerifyIL("Util.ReadOnly", + isMissing ? """ +{ + // Code size 19 (0x13) + .maxstack 4 + .locals init (System.ReadOnlyMemory& V_0) + IL_0000: ldarga.s V_0 + IL_0002: stloc.0 + IL_0003: ldloc.0 + IL_0004: ldc.i4.1 + IL_0005: ldloc.0 + IL_0006: call "int System.ReadOnlyMemory.Length.get" + IL_000b: ldc.i4.1 + IL_000c: sub + IL_000d: call "System.ReadOnlyMemory System.ReadOnlyMemory.Slice(int, int)" + IL_0012: ret +} +""" : """ +{ + // Code size 9 (0x9) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: ldc.i4.1 + IL_0003: call "System.ReadOnlyMemory System.ReadOnlyMemory.Slice(int)" + IL_0008: ret +} +"""); + + verify.VerifyIL("Util.Writable", + isMissing ? """ +{ + // Code size 19 (0x13) + .maxstack 4 + .locals init (System.Memory& V_0) + IL_0000: ldarga.s V_0 + IL_0002: stloc.0 + IL_0003: ldloc.0 + IL_0004: ldc.i4.1 + IL_0005: ldloc.0 + IL_0006: call "int System.Memory.Length.get" + IL_000b: ldc.i4.1 + IL_000c: sub + IL_000d: call "System.Memory System.Memory.Slice(int, int)" + IL_0012: ret +} +""" : """ +{ + // Code size 9 (0x9) + .maxstack 2 + IL_0000: ldarga.s V_0 + IL_0002: ldc.i4.1 + IL_0003: call "System.Memory System.Memory.Slice(int)" + IL_0008: ret +} +"""); + } + + [Fact] + public void SliceStart_05() + { + // Object initializer with `[start..] =` + string source = """ +System.Console.Write(Util.M()); + +static class Util +{ + public static C M() => new C() { [1..] = 42 }; +} + +public class C +{ + private readonly int[] _values = new int[10]; + + public int Length { get { System.Console.Write("Length "); return _values.Length; } } + + public ref int Slice(int start) => throw null; + + public ref int Slice(int start, int length) { System.Console.Write($"SliceStartLength({start},{length}) "); return ref _values[start]; } + + public override string ToString() => _values[1].ToString(); +} +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + var verify = CompileAndVerify(comp, expectedOutput: ExpectedOutput("Length SliceStartLength(1,9) 42"), verify: Verification.Skipped); + + verify.VerifyDiagnostics(); + verify.VerifyIL("Util.M", """ +{ + // Code size 28 (0x1c) + .maxstack 4 + .locals init (int V_0, + int V_1) + IL_0000: newobj "C..ctor()" + IL_0005: dup + IL_0006: ldc.i4.1 + IL_0007: stloc.0 + IL_0008: dup + IL_0009: callvirt "int C.Length.get" + IL_000e: ldc.i4.1 + IL_000f: sub + IL_0010: stloc.1 + IL_0011: ldloc.0 + IL_0012: ldloc.1 + IL_0013: callvirt "ref int C.Slice(int, int)" + IL_0018: ldc.i4.s 42 + IL_001a: stind.i4 + IL_001b: ret +} +"""); + } + + [Fact] + public void SliceStart_06() + { + // `with` expression with `[start..] =` + string source = """ +_ = new C() with { [1..] = 42 }; + +public record class C +{ + public int Length => throw null; + public ref int Slice(int start) => throw null; + public ref int Slice(int start, int length) => throw null; +} +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + comp.VerifyEmitDiagnostics( + // (1,20): error CS0131: The left-hand side of an assignment must be a variable, property or indexer + // _ = new C() with { [1..] = 42 }; + Diagnostic(ErrorCode.ERR_AssgLvalueExpected, "[1..]").WithLocation(1, 20), + // (1,20): error CS0747: Invalid initializer member declarator + // _ = new C() with { [1..] = 42 }; + Diagnostic(ErrorCode.ERR_InvalidInitializerElementInitializer, "[1..] = 42").WithLocation(1, 20)); + } + + [Fact] + public void SliceStart_07() + { + // ReadOnlySpan with Range instance + string source = """ +using System; + +Console.Write(Util.M("0123", 1..).ToString()); +Console.Write(' '); +Console.Write(Util.M("4567", 1..^1).ToString()); + +static class Util +{ + public static ReadOnlySpan M(ReadOnlySpan s, Range range) => s[range]; +} +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + + var verify = CompileAndVerify(comp, expectedOutput: ExpectedOutput("123 56"), verify: Verification.Skipped); + verify.VerifyDiagnostics(); + // Uses Slice(int, int) + verify.VerifyIL("Util.M", """ +{ + // Code size 57 (0x39) + .maxstack 3 + .locals init (System.Range V_0, + int V_1, + int V_2, + int V_3, + System.Index V_4) + IL_0000: ldarga.s V_0 + IL_0002: ldarg.1 + IL_0003: stloc.0 + IL_0004: dup + IL_0005: call "int System.ReadOnlySpan.Length.get" + IL_000a: stloc.1 + IL_000b: ldloca.s V_0 + IL_000d: call "System.Index System.Range.Start.get" + IL_0012: stloc.s V_4 + IL_0014: ldloca.s V_4 + IL_0016: ldloc.1 + IL_0017: call "int System.Index.GetOffset(int)" + IL_001c: stloc.2 + IL_001d: ldloca.s V_0 + IL_001f: call "System.Index System.Range.End.get" + IL_0024: stloc.s V_4 + IL_0026: ldloca.s V_4 + IL_0028: ldloc.1 + IL_0029: call "int System.Index.GetOffset(int)" + IL_002e: ldloc.2 + IL_002f: sub + IL_0030: stloc.3 + IL_0031: ldloc.2 + IL_0032: ldloc.3 + IL_0033: call "System.ReadOnlySpan System.ReadOnlySpan.Slice(int, int)" + IL_0038: ret +} +"""); + } + + [Theory] + [CombinatorialData] + public void SliceStart_08(bool hasSliceInt) + { + // ReadOnlySpan where Slice(int, int) is absent, but Slice(int) may be present + string spanSource = $$""" +namespace System +{ + public readonly ref struct ReadOnlySpan + { + public ref readonly T this[int i] => throw null; + public int Length { get; } + + public ReadOnlySpan(T[] arr) => throw null; + + public ReadOnlySpan(T[] arr, int start, int length) => throw null; + + public static implicit operator ReadOnlySpan(string s) => throw null; + + {{(hasSliceInt + ? "public ReadOnlySpan Slice(int offset) => throw null;" + : "")}} + } +} +"""; + + var spanRef = CreateCompilation( + [spanSource, TestSources.Index, TestSources.Range], + options: TestOptions.UnsafeReleaseDll).VerifyDiagnostics().EmitToImageReference(); + + string source = """ +static class C +{ + public static System.ReadOnlySpan M(System.ReadOnlySpan s) => s[1..]; +} +"""; + + var comp = CreateCompilation(source, references: [spanRef]); + comp.VerifyDiagnostics( + // (3,81): error CS1503: Argument 1: cannot convert from 'System.Range' to 'int' + // public static System.ReadOnlySpan M(System.ReadOnlySpan s) => s[1..]; + Diagnostic(ErrorCode.ERR_BadArgType, "1..").WithArguments("1", "System.Range", "int").WithLocation(3, 81)); + } + + [Fact] + public void SliceStart_09() + { + // SubtractFromLength strategy in start.. + string source = """ +System.Console.Write(GetString()[^GetStart()..]); + +static int GetStart() { System.Console.Write("GetStart "); return 3; } +static string GetString() { System.Console.Write("GetString "); return "123456"; } +"""; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + var verifier = CompileAndVerify(comp, expectedOutput: ExpectedOutput("GetString GetStart 456"), verify: Verification.Skipped); + verifier.VerifyDiagnostics(); + verifier.VerifyIL("", """ +{ + // Code size 30 (0x1e) + .maxstack 3 + .locals init (int V_0) + IL_0000: call "string Program.<
$>g__GetString|0_1()" + IL_0005: call "int Program.<
$>g__GetStart|0_0()" + IL_000a: stloc.0 + IL_000b: dup + IL_000c: callvirt "int string.Length.get" + IL_0011: ldloc.0 + IL_0012: sub + IL_0013: callvirt "string string.Substring(int)" + IL_0018: call "void System.Console.Write(string)" + IL_001d: ret +} +"""); + } + + [Fact] + public void SliceStart_10() + { + // UseGetOffsetAPI strategy in start.. + string source = """ +System.Console.Write(C.M("0123")); + +static class C +{ + public static string M(string s) => s[GetStart()..]; + public static System.Index GetStart() { System.Console.Write("GetStart "); return 1; } +} +"""; + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + var verifier = CompileAndVerify(comp, expectedOutput: ExpectedOutput("GetStart 123"), verify: Verification.Skipped); + verifier.VerifyDiagnostics(); + verifier.VerifyIL("C.M", """ +{ + // Code size 28 (0x1c) + .maxstack 3 + .locals init (string V_0, + System.Index V_1) + IL_0000: ldarg.0 + IL_0001: stloc.0 + IL_0002: ldloc.0 + IL_0003: call "System.Index C.GetStart()" + IL_0008: stloc.1 + IL_0009: ldloca.s V_1 + IL_000b: ldloc.0 + IL_000c: callvirt "int string.Length.get" + IL_0011: call "int System.Index.GetOffset(int)" + IL_0016: callvirt "string string.Substring(int)" + IL_001b: ret +} +"""); + } } } diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/PropertyTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/PropertyTests.cs index 6b4e265dbced..0b30427c365e 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/PropertyTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/PropertyTests.cs @@ -4,6 +4,7 @@ #nullable disable +using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; @@ -49,5 +50,44 @@ .maxstack 2 } "); } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82122")] + public void PropertyIsReadOnly_WithTypeArgumentFromUnrelatedAssembly() + { + string source1 = """ + public class C0 + { + public virtual int P { get; set; } + } + + public class C1 : C0 + { + public override int P { get; set; } + } + """; + var comp1 = CreateCompilation(source1); + comp1.VerifyDiagnostics(); + + string source2 = """ + internal class C2 { } + + internal class C3 : C1 + { + public override int P { get; set; } + } + """; + + var comp2 = CreateCompilation(source2, references: [comp1.ToMetadataReference()]); + comp2.VerifyDiagnostics(); + + // Get the property symbol and check IsReadOnly + var c3 = comp2.GlobalNamespace.GetTypeMember("C3"); + var property = c3.BaseTypeNoUseSiteDiagnostics.GetMember("P"); + + // These should not throw an assertion + Assert.False(property.IsReadOnly); + Assert.False(property.IsWriteOnly); + } } } diff --git a/src/Compilers/CSharp/Test/Emit/CodeGen/UnsafeTests.cs b/src/Compilers/CSharp/Test/Emit/CodeGen/UnsafeTests.cs index 6e9a6c8c86f9..2ec441c0f2ed 100644 --- a/src/Compilers/CSharp/Test/Emit/CodeGen/UnsafeTests.cs +++ b/src/Compilers/CSharp/Test/Emit/CodeGen/UnsafeTests.cs @@ -12,6 +12,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.CodeGen { + [CompilerTrait(CompilerFeature.Unsafe)] public class UnsafeTests : EmitMetadataTestBase { #region AddressOf tests @@ -860,6 +861,192 @@ .maxstack 3 """); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/79051")] + public void Retrack_PointerToRefLocal_Arg_FunctionPointer_ReturnsRef() + { + var source = """ + class C + { + unsafe void M(byte* p, delegate* f) + { + ref byte b = ref f(ref *p); + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 19 (0x13) + .maxstack 2 + .locals init (byte& V_0, //b + delegate* V_1) + IL_0000: nop + IL_0001: ldarg.2 + IL_0002: stloc.1 + IL_0003: ldarg.1 + IL_0004: ldloc.1 + IL_0005: calli "delegate*" + IL_000a: stloc.0 + IL_000b: ldloc.0 + IL_000c: call "string byte.ToString()" + IL_0011: pop + IL_0012: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 16 (0x10) + .maxstack 2 + .locals init (delegate* V_0) + IL_0000: ldarg.2 + IL_0001: stloc.0 + IL_0002: ldarg.1 + IL_0003: ldloc.0 + IL_0004: calli "delegate*" + IL_0009: call "string byte.ToString()" + IL_000e: pop + IL_000f: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/79051")] + public void Retrack_PointerToRefLocal_Arg_FunctionPointer_ReturnsPointer() + { + var source = """ + class C + { + unsafe void M(byte* p, delegate* f) + { + ref byte b = ref *f(ref *p); + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 19 (0x13) + .maxstack 2 + .locals init (byte& V_0, //b + delegate* V_1) + IL_0000: nop + IL_0001: ldarg.2 + IL_0002: stloc.1 + IL_0003: ldarg.1 + IL_0004: ldloc.1 + IL_0005: calli "delegate*" + IL_000a: stloc.0 + IL_000b: ldloc.0 + IL_000c: call "string byte.ToString()" + IL_0011: pop + IL_0012: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 18 (0x12) + .maxstack 2 + .locals init (byte& V_0, //b + delegate* V_1) + IL_0000: ldarg.2 + IL_0001: stloc.1 + IL_0002: ldarg.1 + IL_0003: ldloc.1 + IL_0004: calli "delegate*" + IL_0009: stloc.0 + IL_000a: ldloc.0 + IL_000b: call "string byte.ToString()" + IL_0010: pop + IL_0011: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/79051")] + public void Retrack_PointerToRefLocal_FunctionPointer_ReturnsRef() + { + var source = """ + class C + { + unsafe void M(delegate* f) + { + ref byte b = ref f(); + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 16 (0x10) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: calli "delegate*" + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: call "string byte.ToString()" + IL_000e: pop + IL_000f: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 13 (0xd) + .maxstack 1 + IL_0000: ldarg.1 + IL_0001: calli "delegate*" + IL_0006: call "string byte.ToString()" + IL_000b: pop + IL_000c: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/79051")] + public void Retrack_PointerToRefLocal_FunctionPointer_ReturnsPointer() + { + var source = """ + class C + { + unsafe void M(delegate* f) + { + ref byte b = ref *f(); + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 16 (0x10) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: calli "delegate*" + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: call "string byte.ToString()" + IL_000e: pop + IL_000f: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 15 (0xf) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: calli "delegate*" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: call "string byte.ToString()" + IL_000d: pop + IL_000e: ret + } + """); + } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/79051")] public void Retrack_PointerToRefLocal_Synthesized() { @@ -944,6 +1131,548 @@ .maxstack 1 """); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + public void Retrack_PointerToRefLocal_NestedAssignment() + { + var source = """ + class C + { + unsafe void M(byte* p) + { + ref byte b1 = ref F(); + ref byte b2 = ref (b1 = ref *p); + b2.ToString(); + } + ref byte F() => throw null; + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 20 (0x14) + .maxstack 2 + .locals init (byte& V_0, //b1 + byte& V_1) //b2 + IL_0000: nop + IL_0001: ldarg.0 + IL_0002: call "ref byte C.F()" + IL_0007: stloc.0 + IL_0008: ldarg.1 + IL_0009: dup + IL_000a: stloc.0 + IL_000b: stloc.1 + IL_000c: ldloc.1 + IL_000d: call "string byte.ToString()" + IL_0012: pop + IL_0013: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 19 (0x13) + .maxstack 2 + .locals init (byte& V_0, //b1 + byte& V_1) //b2 + IL_0000: ldarg.0 + IL_0001: call "ref byte C.F()" + IL_0006: stloc.0 + IL_0007: ldarg.1 + IL_0008: dup + IL_0009: stloc.0 + IL_000a: stloc.1 + IL_000b: ldloc.1 + IL_000c: call "string byte.ToString()" + IL_0011: pop + IL_0012: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + public void Retrack_PointerToRefLocal_Field() + { + var source = """ + class C + { + unsafe void M(System.ValueTuple* p) + { + ref byte b = ref p->Item1; + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 16 (0x10) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: ldflda "byte System.ValueTuple.Item1" + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: call "string byte.ToString()" + IL_000e: pop + IL_000f: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 15 (0xf) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: ldflda "byte System.ValueTuple.Item1" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: call "string byte.ToString()" + IL_000d: pop + IL_000e: ret + } + """); + } + + [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + [InlineData("s->F")] + [InlineData("(*s).F")] + [InlineData("s[0].F")] + public void Retrack_PointerToRefLocal_Field_Field(string expr) + { + var source = $$""" + struct S + { + System.ValueTuple F; + unsafe void M(S* s) + { + ref byte b = ref {{expr}}.Item1; + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("S.M", """ + { + // Code size 21 (0x15) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: ldflda "System.ValueTuple S.F" + IL_0007: ldflda "byte System.ValueTuple.Item1" + IL_000c: stloc.0 + IL_000d: ldloc.0 + IL_000e: call "string byte.ToString()" + IL_0013: pop + IL_0014: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("S.M", """ + { + // Code size 20 (0x14) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: ldflda "System.ValueTuple S.F" + IL_0006: ldflda "byte System.ValueTuple.Item1" + IL_000b: stloc.0 + IL_000c: ldloc.0 + IL_000d: call "string byte.ToString()" + IL_0012: pop + IL_0013: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + public void Retrack_PointerToRefLocal_FieldPointer_Field() + { + var source = """ + struct S + { + unsafe System.ValueTuple* F; + unsafe void M(S* s) + { + ref byte b = ref s->F->Item1; + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("S.M", """ + { + // Code size 21 (0x15) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: ldfld "System.ValueTuple* S.F" + IL_0007: ldflda "byte System.ValueTuple.Item1" + IL_000c: stloc.0 + IL_000d: ldloc.0 + IL_000e: call "string byte.ToString()" + IL_0013: pop + IL_0014: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("S.M", """ + { + // Code size 20 (0x14) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: ldfld "System.ValueTuple* S.F" + IL_0006: ldflda "byte System.ValueTuple.Item1" + IL_000b: stloc.0 + IL_000c: ldloc.0 + IL_000d: call "string byte.ToString()" + IL_0012: pop + IL_0013: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + public void Retrack_PointerToRefLocal_Function() + { + var source = """ + struct S + { + unsafe byte* F() => null; + unsafe void M(S* s) + { + ref byte b = ref *s->F(); + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("S.M", """ + { + // Code size 16 (0x10) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: call "byte* S.F()" + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: call "string byte.ToString()" + IL_000e: pop + IL_000f: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("S.M", """ + { + // Code size 15 (0xf) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: call "byte* S.F()" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: call "string byte.ToString()" + IL_000d: pop + IL_000e: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + public void Retrack_PointerToRefLocal_Function_Field() + { + var source = """ + struct S + { + unsafe System.ValueTuple* F() => null; + unsafe void M(S* s) + { + ref byte b = ref s->F()->Item1; + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("S.M", """ + { + // Code size 21 (0x15) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: call "System.ValueTuple* S.F()" + IL_0007: ldflda "byte System.ValueTuple.Item1" + IL_000c: stloc.0 + IL_000d: ldloc.0 + IL_000e: call "string byte.ToString()" + IL_0013: pop + IL_0014: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("S.M", """ + { + // Code size 20 (0x14) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: call "System.ValueTuple* S.F()" + IL_0006: ldflda "byte System.ValueTuple.Item1" + IL_000b: stloc.0 + IL_000c: ldloc.0 + IL_000d: call "string byte.ToString()" + IL_0012: pop + IL_0013: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + public void Retrack_PointerToRefLocal_Conditional_Both() + { + var source = """ + class C + { + unsafe void M(bool c, byte* p1, byte* p2) + { + ref byte b = ref c ? ref *p1 : ref *p2; + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 17 (0x11) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: brtrue.s IL_0007 + IL_0004: ldarg.3 + IL_0005: br.s IL_0008 + IL_0007: ldarg.2 + IL_0008: stloc.0 + IL_0009: ldloc.0 + IL_000a: call "string byte.ToString()" + IL_000f: pop + IL_0010: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 16 (0x10) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: brtrue.s IL_0006 + IL_0003: ldarg.3 + IL_0004: br.s IL_0007 + IL_0006: ldarg.2 + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: call "string byte.ToString()" + IL_000e: pop + IL_000f: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + public void Retrack_PointerToRefLocal_Conditional_One() + { + var source = """ + class C + { + unsafe void M(bool c, ref byte p1, byte* p2) + { + ref byte b = ref c ? ref p1 : ref *p2; + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 17 (0x11) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: brtrue.s IL_0007 + IL_0004: ldarg.3 + IL_0005: br.s IL_0008 + IL_0007: ldarg.2 + IL_0008: stloc.0 + IL_0009: ldloc.0 + IL_000a: call "string byte.ToString()" + IL_000f: pop + IL_0010: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 16 (0x10) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: brtrue.s IL_0006 + IL_0003: ldarg.3 + IL_0004: br.s IL_0007 + IL_0006: ldarg.2 + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: call "string byte.ToString()" + IL_000e: pop + IL_000f: ret + } + """); + } + + [Fact] + public void Retrack_PointerToRefLocal_Conditional_None() + { + var source = """ + class C + { + unsafe void M(bool c, ref byte p1, ref byte p2) + { + ref byte b = ref c ? ref p1 : ref p2; + b.ToString(); + } + } + """; + CompileAndVerify(source, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 17 (0x11) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: brtrue.s IL_0007 + IL_0004: ldarg.3 + IL_0005: br.s IL_0008 + IL_0007: ldarg.2 + IL_0008: stloc.0 + IL_0009: ldloc.0 + IL_000a: call "string byte.ToString()" + IL_000f: pop + IL_0010: ret + } + """); + CompileAndVerify(source, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 14 (0xe) + .maxstack 1 + IL_0000: ldarg.1 + IL_0001: brtrue.s IL_0006 + IL_0003: ldarg.3 + IL_0004: br.s IL_0007 + IL_0006: ldarg.2 + IL_0007: call "string byte.ToString()" + IL_000c: pop + IL_000d: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + public void Retrack_PointerToRefLocal_Conditional_Field_01() + { + var source = """ + class C + { + unsafe void M(bool c, System.ValueTuple* t) + { + ref byte b = ref c ? ref t->Item1 : ref t->Item2; + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 27 (0x1b) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: brtrue.s IL_000c + IL_0004: ldarg.2 + IL_0005: ldflda "byte System.ValueTuple.Item2" + IL_000a: br.s IL_0012 + IL_000c: ldarg.2 + IL_000d: ldflda "byte System.ValueTuple.Item1" + IL_0012: stloc.0 + IL_0013: ldloc.0 + IL_0014: call "string byte.ToString()" + IL_0019: pop + IL_001a: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 26 (0x1a) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: brtrue.s IL_000b + IL_0003: ldarg.2 + IL_0004: ldflda "byte System.ValueTuple.Item2" + IL_0009: br.s IL_0011 + IL_000b: ldarg.2 + IL_000c: ldflda "byte System.ValueTuple.Item1" + IL_0011: stloc.0 + IL_0012: ldloc.0 + IL_0013: call "string byte.ToString()" + IL_0018: pop + IL_0019: ret + } + """); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82137")] + public void Retrack_PointerToRefLocal_Conditional_Field_02() + { + var source = """ + class C + { + unsafe void M(bool c, System.ValueTuple* t1, System.ValueTuple* t2) + { + ref byte b = ref (c ? ref *t1 : ref *t2).Item1; + b.ToString(); + } + } + """; + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeDebugDll).VerifyIL("C.M", """ + { + // Code size 22 (0x16) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: nop + IL_0001: ldarg.1 + IL_0002: brtrue.s IL_0007 + IL_0004: ldarg.3 + IL_0005: br.s IL_0008 + IL_0007: ldarg.2 + IL_0008: ldflda "byte System.ValueTuple.Item1" + IL_000d: stloc.0 + IL_000e: ldloc.0 + IL_000f: call "string byte.ToString()" + IL_0014: pop + IL_0015: ret + } + """); + CompileAndVerify(source, verify: Verification.Fails, options: TestOptions.UnsafeReleaseDll).VerifyIL("C.M", """ + { + // Code size 21 (0x15) + .maxstack 1 + .locals init (byte& V_0) //b + IL_0000: ldarg.1 + IL_0001: brtrue.s IL_0006 + IL_0003: ldarg.3 + IL_0004: br.s IL_0007 + IL_0006: ldarg.2 + IL_0007: ldflda "byte System.ValueTuple.Item1" + IL_000c: stloc.0 + IL_000d: ldloc.0 + IL_000e: call "string byte.ToString()" + IL_0013: pop + IL_0014: ret + } + """); + } + #endregion Dereference tests #region Pointer member access tests diff --git a/src/Compilers/CSharp/Test/Emit2/CodeGen/IndexAndRangeTests.cs b/src/Compilers/CSharp/Test/Emit2/CodeGen/IndexAndRangeTests.cs index 024bf10ea2ca..e831fcaf0b04 100644 --- a/src/Compilers/CSharp/Test/Emit2/CodeGen/IndexAndRangeTests.cs +++ b/src/Compilers/CSharp/Test/Emit2/CodeGen/IndexAndRangeTests.cs @@ -15,6 +15,11 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.CodeGen { public class IndexAndRangeTests : CSharpTestBase { + private static string ExpectedOutput(string output) + { + return ExecutionConditionUtil.IsMonoOrCoreClr ? output : null; + } + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/67533")] public void InObjectInitializer_Index(bool useCsharp13) { @@ -4513,5 +4518,89 @@ public int this[int x] Diagnostic(ErrorCode.ERR_ExpressionTreeContainsAssignment, "[0] = 1").WithLocation(11, 84) ); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82793")] + public void ImplicitRangeIndexer_ObjectInitializer_01() + { + // Object initializer on C with `[GetStart()..] = value` + string source = """ +System.Console.Write(Util.M()); + +static class Util +{ + public static C M() => new C() { [GetStart()..] = 42 }; + + private static int GetStart() + { + System.Console.Write("GetStart "); + return 1; + } +} + +public class C +{ + private readonly int[] _values = new int[4]; + + public int Length { get { System.Console.Write("Length "); return _values.Length; } } + + public ref int Slice(int start, int length) + { + System.Console.Write($"SliceStartLength({start}, {length}) "); + return ref _values[start]; + } + + public override string ToString() => _values[1].ToString(); +} +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + var verify = CompileAndVerify(comp, expectedOutput: ExpectedOutput("GetStart Length SliceStartLength(1, 3) 42"), verify: Verification.Skipped); + verify.VerifyDiagnostics(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82793")] + public void ImplicitRangeIndexer_ObjectInitializer_02() + { + // Object initializer on C with `[GetStart()..] = { P1 = 1, P2 = 2 }` + string source = """ +System.Console.Write(Util.M()); + +static class Util +{ + public static C M() => new C() { [GetStart()..] = { P1 = 1, P2 = 2 } }; + + private static int GetStart() + { + System.Console.Write("GetStart "); + return 1; + } +} + +public class C +{ + private readonly Item[] _values = new[] { new Item(), new Item(), new Item(), new Item() }; + + public int Length { get { System.Console.Write("Length "); return _values.Length; } } + + public ref Item Slice(int start, int length) + { + System.Console.Write($"SliceStartLength({start}, {length}) "); + return ref _values[start]; + } + + public override string ToString() => (_values[1].P1, _values[1].P2).ToString(); +} + +public class Item +{ + public int P1 { get; set; } + public int P2 { get; set; } +} +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + var verify = CompileAndVerify(comp, expectedOutput: ExpectedOutput("GetStart Length SliceStartLength(1, 3) SliceStartLength(1, 3) (1, 2)"), verify: Verification.Skipped); + verify.VerifyDiagnostics(); + } } } diff --git a/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueClosureTests.cs b/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueClosureTests.cs index 4814a60d4208..285176817f0f 100644 --- a/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueClosureTests.cs +++ b/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueClosureTests.cs @@ -5472,7 +5472,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Capture_ConstructorParameter() { using var _ = new EditAndContinueTest() @@ -6554,7 +6554,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void CeaseCapture_LastLocal_Lambda() { using var _ = new EditAndContinueTest() @@ -6754,7 +6754,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void CeaseCapture_LastLocal_LocalFunction() { using var _ = new EditAndContinueTest() @@ -9059,7 +9059,7 @@ .maxstack 2 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void ChangeLambdaParent_LambdaAndLocalFunction_Lambda() { using var _ = new EditAndContinueTest() @@ -9800,7 +9800,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Closure_ClassToStruct() { using var _ = new EditAndContinueTest() @@ -10329,7 +10329,7 @@ .maxstack 8 /// Some lambda rude edits are simpler to detect in the IDE. They are specified via . /// The IDE tests cover the specific cases. /// - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void IdeDetectedRuntimeRudeEdit() { using var _ = new EditAndContinueTest() diff --git a/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueStateMachineTests.cs b/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueStateMachineTests.cs index b5a75239170f..f8634a1e2e5a 100644 --- a/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueStateMachineTests.cs +++ b/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueStateMachineTests.cs @@ -2670,7 +2670,7 @@ .locals init (int V_0, "); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void UpdateAsync_Await_Remove_TryBlock() { var source0 = MarkedSource(@" @@ -10838,7 +10838,7 @@ public IEnumerable F() diff1.EmitResult.Diagnostics.Verify(); } - [Fact, WorkItem(9119, "https://github.com/dotnet/roslyn/issues/9119")] + [ConditionalFact(typeof(IsEnglishLocal)), WorkItem(9119, "https://github.com/dotnet/roslyn/issues/9119"), WorkItem(82612, "https://github.com/dotnet/roslyn/issues/82612")] public void MissingAsyncStateMachineAttribute() { var source0 = MarkedSource(@" @@ -10896,7 +10896,7 @@ public async Task F() Diagnostic(ErrorCode.ERR_EncUpdateFailedMissingSymbol, "F").WithArguments("attribute", "System.Runtime.CompilerServices.AsyncStateMachineAttribute").WithLocation(6, 28)); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal)), WorkItem(82612, "https://github.com/dotnet/roslyn/issues/82612")] public void AddedAsyncStateMachineAttribute() { var source0 = MarkedSource(@" @@ -11072,7 +11072,7 @@ public async Task F() diff1.EmitResult.Diagnostics.Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal)), WorkItem(82612, "https://github.com/dotnet/roslyn/issues/82612")] public void NonAsyncToAsync_MissingAttribute() { var source0 = MarkedSource(@" diff --git a/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueTests.cs b/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueTests.cs index dfaf61c65cdb..5c76b2649d48 100644 --- a/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueTests.cs +++ b/src/Compilers/CSharp/Test/Emit2/Emit/EditAndContinue/EditAndContinueTests.cs @@ -4628,7 +4628,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Property_DeleteAndAdd_WithAccessorBodies() { using var _ = new EditAndContinueTest() @@ -4787,7 +4787,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Property_DeleteAndAdd_OneAccessor() { using var _ = new EditAndContinueTest() @@ -5199,7 +5199,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Property_ChangeReturnType() { using var _ = new EditAndContinueTest() @@ -5442,7 +5442,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Property_Rename() { using var _ = new EditAndContinueTest() @@ -5812,7 +5812,7 @@ .maxstack 2 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Indexer_ChangeParameterType() { using var _ = new EditAndContinueTest() @@ -6495,7 +6495,7 @@ .maxstack 2 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Event_Rename() { using var _ = new EditAndContinueTest() @@ -17162,7 +17162,7 @@ class C expectedErrors: [ // error CS7043: Cannot emit update; constructor 'System.Exception..ctor(string)' is missing. - Diagnostic(ErrorCode.ERR_EncUpdateFailedMissingSymbol).WithArguments("constructor", "System.Exception..ctor(string)").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_EncUpdateFailedMissingSymbol).WithArguments(CodeAnalysisResources.Constructor, "System.Exception..ctor(string)").WithLocation(1, 1) ]) .Verify(); } @@ -17213,7 +17213,7 @@ class C expectedErrors: [ // error CS7043: Cannot emit update; method 'void System.Action.Invoke(T arg)' is missing. - Diagnostic(ErrorCode.ERR_EncUpdateFailedMissingSymbol).WithArguments("method", "void System.Action.Invoke(T arg)").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_EncUpdateFailedMissingSymbol).WithArguments(CodeAnalysisResources.Method, "void System.Action.Invoke(T arg)").WithLocation(1, 1) ]) .Verify(); } @@ -17296,7 +17296,7 @@ .maxstack 2 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_Delete_PredefinedHotReloadException() { var exceptionSource = """ @@ -17380,7 +17380,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_Delete_PredefinedHotReloadException_Inserted() { var exceptionSource = """ @@ -17519,7 +17519,7 @@ void F2() {} .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_Delete_PredefinedHotReloadException_DataSectionLiterals() { var parseOptions = TestOptions.Regular.WithFeature(Feature.ExperimentalDataSectionStringLiterals, "0"); @@ -18431,7 +18431,7 @@ .maxstack 1 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_Delete_WithLambda() { using var _ = new EditAndContinueTest() @@ -18680,7 +18680,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_Delete_WithLambda_AddedMethod() { using var _ = new EditAndContinueTest() @@ -18817,7 +18817,7 @@ .maxstack 2 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_Delete_WithLambda_MultipleGenerations() { var common = """ @@ -19147,7 +19147,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_Delete_WithLocalFunction_MultipleGenerations() { var common = """ @@ -19557,7 +19557,7 @@ class C test.Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_ChangeParameterType() { using var _ = new EditAndContinueTest() @@ -19721,7 +19721,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_ChangeReturnType() { using var _ = new EditAndContinueTest() @@ -19893,7 +19893,7 @@ .maxstack 8 .Verify(); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void Method_InsertAndDeleteParameter() { using var _ = new EditAndContinueTest() diff --git a/src/Compilers/CSharp/Test/Emit2/Emit/LocalStateTracing/LocalStateTracingTests.cs b/src/Compilers/CSharp/Test/Emit2/Emit/LocalStateTracing/LocalStateTracingTests.cs index c9973dc4bfc3..4498dfd72f54 100644 --- a/src/Compilers/CSharp/Test/Emit2/Emit/LocalStateTracing/LocalStateTracingTests.cs +++ b/src/Compilers/CSharp/Test/Emit2/Emit/LocalStateTracing/LocalStateTracingTests.cs @@ -4345,6 +4345,116 @@ .locals init (Microsoft.CodeAnalysis.Runtime.LocalStoreTracker V_0, "); } + [Fact] + public void RuntimeAsync_Task() + { + var source = WithHelpers(@" +using System.Threading.Tasks; + +class C +{ + static async Task M(int p) + { + int a = p; + F(out var b); + await Task.CompletedTask; + int c = b; + } + + static int F(out int a) => a = 1; + static async Task Main() => await M(2); +} +"); + + var compilation = CreateRuntimeAsyncCompilation(source, options: TestOptions.UnsafeDebugExe); + var ilVerifyMessage = """ + [M]: Return value missing on the stack. { Offset = 0x55 } + [Main]: Return value missing on the stack. { Offset = 0x22 } + """; + var verifier = CompileAndVerify( + compilation, + emitOptions: s_emitOptions, + verify: s_verification with { ILVerifyMessage = ilVerifyMessage + Environment.NewLine + s_verification.ILVerifyMessage }, + expectedOutput: RuntimeAsyncTestHelpers.ExpectedOutput(@" +Main: Entered +M: Entered +M: P'p'[0] = 2 +M: L1 = 2 +F: Entered +F: P'a'[0] = 1 +F: Returned +M: L2 = 1 +M: L3 = 1 +M: Returned +Main: Returned +")); + + verifier.VerifyMethodBody("C.M", @" +{ + // Code size 86 (0x56) + .maxstack 3 + .locals init (Microsoft.CodeAnalysis.Runtime.LocalStoreTracker V_0, + int V_1, //a + int V_2, //b + int V_3) //c + // sequence point: + IL_0000: ldtoken ""System.Threading.Tasks.Task C.M(int)"" + IL_0005: call ""Microsoft.CodeAnalysis.Runtime.LocalStoreTracker Microsoft.CodeAnalysis.Runtime.LocalStoreTracker.LogMethodEntry(int)"" + IL_000a: stloc.0 + .try + { + // sequence point: { + IL_000b: ldloca.s V_0 + IL_000d: ldarg.0 + IL_000e: ldc.i4.0 + IL_000f: call ""void Microsoft.CodeAnalysis.Runtime.LocalStoreTracker.LogParameterStore(uint, int)"" + IL_0014: nop + // sequence point: int a = p; + IL_0015: ldloca.s V_0 + IL_0017: ldarg.0 + IL_0018: dup + IL_0019: stloc.1 + IL_001a: ldc.i4.1 + IL_001b: call ""void Microsoft.CodeAnalysis.Runtime.LocalStoreTracker.LogLocalStore(uint, int)"" + IL_0020: nop + // sequence point: F(out var b); + IL_0021: ldloca.s V_2 + IL_0023: call ""int C.F(out int)"" + IL_0028: pop + IL_0029: ldloca.s V_0 + IL_002b: ldloc.2 + IL_002c: ldc.i4.2 + IL_002d: call ""void Microsoft.CodeAnalysis.Runtime.LocalStoreTracker.LogLocalStore(uint, int)"" + IL_0032: nop + // sequence point: await Task.CompletedTask; + IL_0033: call ""System.Threading.Tasks.Task System.Threading.Tasks.Task.CompletedTask.get"" + IL_0038: call ""void System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.Task)"" + IL_003d: nop + // sequence point: int c = b; + IL_003e: ldloca.s V_0 + IL_0040: ldloc.2 + IL_0041: dup + IL_0042: stloc.3 + IL_0043: ldc.i4.3 + IL_0044: call ""void Microsoft.CodeAnalysis.Runtime.LocalStoreTracker.LogLocalStore(uint, int)"" + IL_0049: nop + // sequence point: } + IL_004a: leave.s IL_0055 + } + finally + { + // sequence point: + IL_004c: ldloca.s V_0 + IL_004e: call ""void Microsoft.CodeAnalysis.Runtime.LocalStoreTracker.LogReturn()"" + IL_0053: nop + IL_0054: endfinally + } + // sequence point: } + IL_0055: ret +} +"); + } + [Fact] public void StateMachine_Iterator() { diff --git a/src/Compilers/CSharp/Test/Emit2/PDB/CheckSumTest.cs b/src/Compilers/CSharp/Test/Emit2/PDB/CheckSumTest.cs index 864c10a381cb..f6cc705cd5cf 100644 --- a/src/Compilers/CSharp/Test/Emit2/PDB/CheckSumTest.cs +++ b/src/Compilers/CSharp/Test/Emit2/PDB/CheckSumTest.cs @@ -31,15 +31,22 @@ public void ChecksumAlgorithms() { var source1 = "public class C1 { public C1() { } }"; var source256 = "public class C256 { public C256() { } }"; + var source384 = "public class C384 { public C384() { } }"; + var source512 = "public class C512 { public C512() { } }"; var tree1 = SyntaxFactory.ParseSyntaxTree(StringText.From(source1, Encoding.UTF8, SourceHashAlgorithm.Sha1), path: "sha1.cs"); var tree256 = SyntaxFactory.ParseSyntaxTree(StringText.From(source256, Encoding.UTF8, SourceHashAlgorithm.Sha256), path: "sha256.cs"); + var tree384 = SyntaxFactory.ParseSyntaxTree(StringText.From(source384, Encoding.UTF8, SourceHashAlgorithm.Sha384), path: "sha384.cs"); + var tree512 = SyntaxFactory.ParseSyntaxTree(StringText.From(source512, Encoding.UTF8, SourceHashAlgorithm.Sha512), path: "sha512.cs"); + + var compilation = CreateCompilation(new[] { tree1, tree256, tree384, tree512 }); - var compilation = CreateCompilation(new[] { tree1, tree256 }); compilation.VerifyPdb(@" + + @@ -62,6 +69,24 @@ public void ChecksumAlgorithms() + + + + + + + + + + + + + + + + + + "); } @@ -495,5 +520,98 @@ void M() "); } + + [Fact] + public void CheckSumPragma_Sha384AndSha512Guids() + { + // #pragma checksum with well-known SHA-384/SHA-512 GUIDs and correct-length checksums + var text = """ +class C +{ +#pragma checksum "sha384.cs" "{d99cfeb1-8c43-444a-8a6c-b61269d2a0bf}" "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f" +#pragma checksum "sha512.cs" "{ef2d1afc-6550-46d6-b14b-d70afe9a5566}" "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" + + static void Main() + { +#line 1 "sha384.cs" + System.Console.Write(1); +#line 1 "sha512.cs" + System.Console.Write(2); + } +} +"""; + + var compilation = CreateCompilation(text, options: TestOptions.DebugExe); + compilation.VerifyDiagnostics(); + compilation.VerifyPdb("C.Main", """ + + + + + + + + + + + + + + + + + + + + + + + +"""); + } + + [Fact] + public void CheckSumPragma_UnrecognizedGuid() + { + // #pragma checksum treats GUIDs as opaque values + var text = """ +class C +{ +#pragma checksum "random.cs" "{aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa}" "ef00112233" + + static void Main() + { +#line 1 "random.cs" + System.Console.Write(1); + } +} +"""; + + var compilation = CreateCompilation(text, options: TestOptions.DebugExe); + compilation.VerifyDiagnostics(); + compilation.VerifyPdb("C.Main", """ + + + + + + + + + + + + + + + + + + + + + +"""); + } } } diff --git a/src/Compilers/CSharp/Test/Emit3/Attributes/AttributeTests_RefSafetyRules.cs b/src/Compilers/CSharp/Test/Emit3/Attributes/AttributeTests_RefSafetyRules.cs index 8510c12de27b..5c1114061e16 100644 --- a/src/Compilers/CSharp/Test/Emit3/Attributes/AttributeTests_RefSafetyRules.cs +++ b/src/Compilers/CSharp/Test/Emit3/Attributes/AttributeTests_RefSafetyRules.cs @@ -53,6 +53,30 @@ public void ExplicitAttribute_FromMetadata(bool useCompilationReference) CompileAndVerify(comp, symbolValidator: m => AssertRefSafetyRulesAttribute(m, includesAttributeDefinition: false, includesAttributeUse: true, publicDefinition: true)); } + [Theory] + [CombinatorialData] + public void ExplicitAttribute_FromMetadata_Multiple(bool useCompilationReference) + { + var comp = CreateCompilation(RefSafetyRulesAttributeDefinition, parseOptions: TestOptions.Regular10); + var ref1 = AsReference(comp, useCompilationReference); + + comp = CreateCompilation(RefSafetyRulesAttributeDefinition, parseOptions: TestOptions.Regular10); + var ref2 = AsReference(comp, useCompilationReference); + + var source = +@"public class A +{ + public static ref T F(out T t) => throw null; +}"; + + comp = CreateCompilation(source, references: new[] { ref1, ref2 }, parseOptions: TestOptions.Regular10); + CompileAndVerify(comp, symbolValidator: m => AssertRefSafetyRulesAttribute(m, includesAttributeDefinition: false, includesAttributeUse: false, publicDefinition: false)); + + // Ambiguous attribute definitions from references => synthesize our own. + comp = CreateCompilation(source, references: new[] { ref1, ref2 }); + CompileAndVerify(comp, symbolValidator: m => AssertRefSafetyRulesAttribute(m, includesAttributeDefinition: true, includesAttributeUse: true, publicDefinition: false)); + } + [Fact] public void ExplicitAttribute_MissingConstructor() { diff --git a/src/Compilers/CSharp/Test/Emit3/Attributes/AttributeTests_Synthesized.cs b/src/Compilers/CSharp/Test/Emit3/Attributes/AttributeTests_Synthesized.cs index c92d68cf1a4e..888dd4121642 100644 --- a/src/Compilers/CSharp/Test/Emit3/Attributes/AttributeTests_Synthesized.cs +++ b/src/Compilers/CSharp/Test/Emit3/Attributes/AttributeTests_Synthesized.cs @@ -56,6 +56,20 @@ public static IEnumerable FullMatrixTheoryData } } } + + public static IEnumerable RuntimeAsyncSuppressionAndOptimizationLevelTheoryData + { + get + { + foreach (bool suppressRuntimeAsync in new[] { false, true }) + { + foreach (var level in Enum.GetValues(typeof(OptimizationLevel))) + { + yield return new object[] { level, suppressRuntimeAsync }; + } + } + } + } #endregion #region Helpers @@ -1949,16 +1963,17 @@ public void AsyncStateMachineAttribute_RuntimeAsync_TLSEntrypoint(OptimizationLe } [Theory] - [MemberData(nameof(OptimizationLevelTheoryData))] - public void AsyncStateMachineAttribute_RuntimeAsync_ExtensionBlockMember(OptimizationLevel optimizationLevel) + [MemberData(nameof(RuntimeAsyncSuppressionAndOptimizationLevelTheoryData))] + public void AsyncStateMachineAttribute_RuntimeAsync_ExtensionBlockMember(OptimizationLevel optimizationLevel, bool suppressRuntimeAsync) { - string source = """ + string source = $$""" using System.Threading.Tasks; public static class Ex { extension(object o) { + {{(suppressRuntimeAsync ? "[System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)]" : "")}} public async Task F() { await Task.Delay(0); @@ -1970,28 +1985,35 @@ public async Task F() var options = TestOptions.CreateTestOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel) .WithMetadataImportOptions(MetadataImportOptions.All); - var compilation = CreateRuntimeAsyncCompilation(source, options); - var verifier = CompileAndVerify(compilation, verify: Verification.Skipped, symbolValidator: static module => + var compilation = CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition], options); + var verifier = CompileAndVerify(compilation, verify: Verification.Skipped, symbolValidator: module => { var type = module.GlobalNamespace.GetMember("Ex"); var asyncImplMethod = type.GetMember("F"); - // When runtime async is enabled, no state machine is generated, - // so there should be no AsyncStateMachineAttribute and no DebuggerStepThroughAttribute - Assert.Empty(asyncImplMethod.GetAttributes()); + if (suppressRuntimeAsync) + { + Assert.Contains(asyncImplMethod.GetAttributes(), static a => a.AttributeClass?.Name == "AsyncStateMachineAttribute"); + } + else + { + // When runtime async is enabled, no state machine is generated, + // so there should be no AsyncStateMachineAttribute and no DebuggerStepThroughAttribute + Assert.Empty(asyncImplMethod.GetAttributes()); + } - var extension = type.GetTypeMembers().Single(); + var extension = type.GetTypeMembers().Single(static typeMember => typeMember.GetMembers("F").Any()); var asyncExtensionSignatureMethod = extension.GetMember("F"); - Assert.Empty(asyncExtensionSignatureMethod.GetAttributes()); + Assert.DoesNotContain(asyncExtensionSignatureMethod.GetAttributes(), static a => a.AttributeClass?.Name == "AsyncStateMachineAttribute"); }); verifier.VerifyDiagnostics(); } [Theory] - [MemberData(nameof(OptimizationLevelTheoryData))] - public void AsyncStateMachineAttribute_RuntimeAsync_ExtensionBlockMember_WithLambda(OptimizationLevel optimizationLevel) + [MemberData(nameof(RuntimeAsyncSuppressionAndOptimizationLevelTheoryData))] + public void AsyncStateMachineAttribute_RuntimeAsync_ExtensionBlockMember_WithLambda(OptimizationLevel optimizationLevel, bool suppressRuntimeAsync) { - string source = """ + string source = $$""" using System.Threading.Tasks; public static class Ex @@ -2000,7 +2022,7 @@ public static class Ex { public async Task F() { - var f = async () => { await Task.Delay(0); }; + var f = {{(suppressRuntimeAsync ? "[System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)] " : "")}}async () => { await Task.Delay(0); }; await f(); } } @@ -2010,23 +2032,30 @@ public async Task F() var options = TestOptions.CreateTestOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel) .WithMetadataImportOptions(MetadataImportOptions.All); - var compilation = CreateRuntimeAsyncCompilation(source, options); - var verifier = CompileAndVerify(compilation, verify: Verification.Skipped, symbolValidator: static module => + var compilation = CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition], options); + var verifier = CompileAndVerify(compilation, verify: Verification.Skipped, symbolValidator: module => { var type = module.GlobalNamespace.GetMember("Ex"); var typeMembers = type.GetTypeMembers(); AssertEx.SequenceEqual(["Ex.$C43E2675C7BBF9284AF22FB8A9BF0280.$119AA281C143547563250CAF89B48A76", "Ex.<>c"], typeMembers.ToTestDisplayStrings()); var asyncLambda = typeMembers[1].GetMember("b__1_0"); - Assert.Empty(asyncLambda.GetAttributes()); + if (suppressRuntimeAsync) + { + Assert.Contains(asyncLambda.GetAttributes(), static a => a.AttributeClass?.Name == "AsyncStateMachineAttribute"); + } + else + { + Assert.Empty(asyncLambda.GetAttributes()); + } }); verifier.VerifyDiagnostics(); } [Theory] - [MemberData(nameof(OptimizationLevelTheoryData))] - public void AsyncStateMachineAttribute_RuntimeAsync_ExtensionBlockMember_WithLocalFunction(OptimizationLevel optimizationLevel) + [MemberData(nameof(RuntimeAsyncSuppressionAndOptimizationLevelTheoryData))] + public void AsyncStateMachineAttribute_RuntimeAsync_ExtensionBlockMember_WithLocalFunction(OptimizationLevel optimizationLevel, bool suppressRuntimeAsync) { - string source = """ + string source = $$""" using System.Threading.Tasks; public static class Ex @@ -2036,6 +2065,7 @@ public static class Ex public async Task F() { await LocalAsync(); + {{(suppressRuntimeAsync ? "[System.Runtime.CompilerServices.RuntimeAsyncMethodGeneration(false)]" : "")}} async Task LocalAsync() { await Task.Delay(0); } } } @@ -2045,13 +2075,23 @@ public async Task F() var options = TestOptions.CreateTestOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel) .WithMetadataImportOptions(MetadataImportOptions.All); - var compilation = CreateRuntimeAsyncCompilation(source, options); - var verifier = CompileAndVerify(compilation, verify: Verification.Skipped, symbolValidator: static module => + var compilation = suppressRuntimeAsync + ? CreateRuntimeAsyncCompilation([source, RuntimeAsyncMethodGenerationAttributeDefinition], options) + : CreateRuntimeAsyncCompilation(source, options); + var verifier = CompileAndVerify(compilation, verify: Verification.Skipped, symbolValidator: module => { var type = module.GlobalNamespace.GetMember("Ex"); - Assert.Single(type.GetTypeMembers()); + Assert.Equal(suppressRuntimeAsync ? 2 : 1, type.GetTypeMembers().Length); var localFunction = type.GetMember("g__LocalAsync|1_0"); - AssertEx.SequenceEqual(["System.Runtime.CompilerServices.CompilerGeneratedAttribute..ctor()"], localFunction.GetAttributes().SelectAsArray(a => a.AttributeConstructor.ToTestDisplayString())); + Assert.Contains(localFunction.GetAttributes(), static a => a.AttributeClass?.Name == "CompilerGeneratedAttribute"); + if (suppressRuntimeAsync) + { + Assert.Contains(localFunction.GetAttributes(), static a => a.AttributeClass?.Name == "AsyncStateMachineAttribute"); + } + else + { + Assert.DoesNotContain(localFunction.GetAttributes(), static a => a.AttributeClass?.Name == "AsyncStateMachineAttribute"); + } }); verifier.VerifyDiagnostics(); } diff --git a/src/Compilers/CSharp/Test/Emit3/Attributes/InternalsVisibleToAndStrongNameTests.cs b/src/Compilers/CSharp/Test/Emit3/Attributes/InternalsVisibleToAndStrongNameTests.cs index d0df1ed86a85..ae6cd8a627b2 100644 --- a/src/Compilers/CSharp/Test/Emit3/Attributes/InternalsVisibleToAndStrongNameTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Attributes/InternalsVisibleToAndStrongNameTests.cs @@ -185,7 +185,7 @@ public void PubKeyFromKeyFileAttribute_AssemblyKeyFileResolver_RelativeToCurrent Assert.True(ByteSequenceComparer.Equals(s_publicKey, comp.Assembly.Identity.PublicKey)); } - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void PubKeyFromKeyFileAttribute_SigningTempPathNotAvailable() { string code = String.Format("{0}{1}{2}", @"[assembly: System.Reflection.AssemblyKeyFile(@""", s_keyPairFile, @""")] public class C {}"); diff --git a/src/Compilers/CSharp/Test/Emit3/FieldKeywordTests.cs b/src/Compilers/CSharp/Test/Emit3/FieldKeywordTests.cs index cbd54a63f5f1..16e8f3139539 100644 --- a/src/Compilers/CSharp/Test/Emit3/FieldKeywordTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/FieldKeywordTests.cs @@ -12942,6 +12942,85 @@ public string Prop expected: [Diagnostic("TEST_Invalid", "field").WithLocation(5, 16)]); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80504")] + public void SetsRequiredMembers_Subtype_NullResilientBaseProp() + { + // Exercise a corner case involving SetsRequiredMembers, null-resilient getters, and inheritance. + var source1 = """ + #nullable enable + + public class Foo { + public required string Bar { + get => field ?? "a"; + init => field = value; + } + } + """; + + var source2 = """ + #nullable enable + using System.Diagnostics.CodeAnalysis; + + public class FooDerivative : Foo { + [SetsRequiredMembers] + public FooDerivative() { + } + } + """; + + var comp = CreateCompilation([source1, source2], targetFramework: TargetFramework.Net100); + comp.VerifyEmitDiagnostics( + // (6,12): warning CS8618: Non-nullable property 'Bar' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + // public FooDerivative() { + Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "FooDerivative").WithArguments("property", "Bar").WithLocation(6, 12)); + + var comp1 = CreateCompilation(source1, targetFramework: TargetFramework.Net100); + comp1.VerifyEmitDiagnostics(); + + verify2(comp1.ToMetadataReference()); + verify2(comp1.EmitToImageReference()); + + void verify2(MetadataReference reference) + { + var comp2 = CreateCompilation(source2, references: [reference], targetFramework: TargetFramework.Net100); + comp2.VerifyEmitDiagnostics( + // (6,12): warning CS8618: Non-nullable property 'Bar' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + // public FooDerivative() { + Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "FooDerivative").WithArguments("property", "Bar").WithLocation(6, 12)); + } + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/80504")] + public void SetsRequiredMembersInSubtype() + { + var source = """ + #nullable enable + using System.Diagnostics.CodeAnalysis; + + public class Foo { + public required string Bar { + get; + init { + field = value; + } + } + } + + public class FooDerivative : Foo { + + [SetsRequiredMembers] + public FooDerivative() { + } + } + """; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + comp.VerifyEmitDiagnostics( + // (16,12): warning CS8618: Non-nullable property 'Bar' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + // public FooDerivative() { + Diagnostic(ErrorCode.WRN_UninitializedNonNullableField, "FooDerivative").WithArguments("property", "Bar").WithLocation(16, 12)); + } + private class TestAnalyzer1 : DiagnosticAnalyzer { public static readonly DiagnosticDescriptor Descriptor_Field = new(id: "TEST_Field", title: "Test", messageFormat: "", category: "", DiagnosticSeverity.Warning, isEnabledByDefault: true); diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionOperatorsTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionOperatorsTests.cs index 57e6b2363f4d..4b17a089297e 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionOperatorsTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionOperatorsTests.cs @@ -1731,16 +1731,6 @@ static void Main() """; var comp = CreateCompilation(src, options: TestOptions.DebugExe); -#if DEBUG - comp.VerifyEmitDiagnostics( - // (35,13): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions2.extension(C1).operator -(C1)' and 'Extensions1.extension(C1).operator -(C1)' - // _ = -c1; - Diagnostic(ErrorCode.ERR_AmbigOperator, "-").WithArguments("Extensions2.extension(C1).operator -(C1)", "Extensions1.extension(C1).operator -(C1)").WithLocation(35, 13), - // (39,17): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions1.extension(C1).operator checked -(C1)' and 'Extensions2.extension(C1).operator -(C1)' - // _ = -c1; - Diagnostic(ErrorCode.ERR_AmbigOperator, "-").WithArguments("Extensions1.extension(C1).operator checked -(C1)", "Extensions2.extension(C1).operator -(C1)").WithLocation(39, 17) - ); -#else comp.VerifyEmitDiagnostics( // (35,13): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions1.extension(C1).operator -(C1)' and 'Extensions2.extension(C1).operator -(C1)' // _ = -c1; @@ -1749,7 +1739,6 @@ static void Main() // _ = -c1; Diagnostic(ErrorCode.ERR_AmbigOperator, "-").WithArguments("Extensions1.extension(C1).operator checked -(C1)", "Extensions2.extension(C1).operator -(C1)").WithLocation(39, 17) ); -#endif var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); @@ -4268,19 +4257,11 @@ static void Main() """ + CompilerFeatureRequiredAttribute; var comp = CreateCompilation(src, options: TestOptions.DebugExe); -#if DEBUG - comp.VerifyDiagnostics( - // (26,13): error CS9339: Operator resolution is ambiguous between the following members: 'Extensions2.extension(S2).operator -(S2)' and 'Extensions1.extension(S2).operator -(S2)' - // _ = -s2; - Diagnostic(ErrorCode.ERR_AmbigOperator, "-").WithArguments("Extensions2.extension(S2).operator -(S2)", "Extensions1.extension(S2).operator -(S2)").WithLocation(26, 13) - ); -#else comp.VerifyDiagnostics( // (26,13): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions1.extension(S2).operator -(S2)' and 'Extensions2.extension(S2).operator -(S2)' // _ = -s2; Diagnostic(ErrorCode.ERR_AmbigOperator, "-").WithArguments("Extensions1.extension(S2).operator -(S2)", "Extensions2.extension(S2).operator -(S2)").WithLocation(26, 13) ); -#endif var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); @@ -4290,13 +4271,8 @@ static void Main() Assert.Null(symbolInfo.Symbol); Assert.Equal(CandidateReason.Ambiguous, symbolInfo.CandidateReason); -#if DEBUG // Collection of extension blocks depends on GetTypeMembersUnordered for namespace, which conditionally de-orders types for DEBUG only. - AssertEx.Equal("Extensions2.extension(S2).operator -(S2)", symbolInfo.CandidateSymbols[0].ToDisplayString()); - AssertEx.Equal("Extensions1.extension(S2).operator -(S2)", symbolInfo.CandidateSymbols[1].ToDisplayString()); -#else AssertEx.Equal("Extensions1.extension(S2).operator -(S2)", symbolInfo.CandidateSymbols[0].ToDisplayString()); AssertEx.Equal("Extensions2.extension(S2).operator -(S2)", symbolInfo.CandidateSymbols[1].ToDisplayString()); -#endif } [Theory] @@ -7664,16 +7640,6 @@ static void Main() """; var comp = CreateCompilation(src, options: TestOptions.DebugExe); -#if DEBUG - comp.VerifyEmitDiagnostics( - // (35,13): error CS9339: Operator resolution is ambiguous between the following members: 'Extensions2.extension(C1).operator --(C1)' and 'Extensions1.extension(C1).operator --(C1)' - // _ = --c1; - Diagnostic(ErrorCode.ERR_AmbigOperator, "--").WithArguments("Extensions2.extension(C1).operator --(C1)", "Extensions1.extension(C1).operator --(C1)").WithLocation(35, 13), - // (39,17): error CS9339: Operator resolution is ambiguous between the following members: 'Extensions1.extension(C1).operator checked --(C1)' and 'Extensions2.extension(C1).operator --(C1)' - // _ = --c1; - Diagnostic(ErrorCode.ERR_AmbigOperator, "--").WithArguments("Extensions1.extension(C1).operator checked --(C1)", "Extensions2.extension(C1).operator --(C1)").WithLocation(39, 17) - ); -#else comp.VerifyEmitDiagnostics( // (35,13): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions1.extension(C1).operator --(C1)' and 'Extensions2.extension(C1).operator --(C1)' // _ = --c1; @@ -7682,7 +7648,6 @@ static void Main() // _ = --c1; Diagnostic(ErrorCode.ERR_AmbigOperator, "--").WithArguments("Extensions1.extension(C1).operator checked --(C1)", "Extensions2.extension(C1).operator --(C1)").WithLocation(39, 17) ); -#endif var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); @@ -7745,17 +7710,6 @@ static void Main() var comp = CreateCompilation(src, options: TestOptions.DebugExe); -#if DEBUG // Collection of extension blocks depends on GetTypeMembersUnordered for namespace, which conditionally de-orders types for DEBUG only. - comp.VerifyEmitDiagnostics( - // (32,13): error CS0121: The call is ambiguous between the following methods or properties: 'Extensions2.extension(C1).operator --()' and 'Extensions1.extension(C1).operator --()' - // _ = --c1; - Diagnostic(ErrorCode.ERR_AmbigCall, "--").WithArguments("Extensions2.extension(C1).operator --()", "Extensions1.extension(C1).operator --()").WithLocation(32, 13), - // (36,17): error CS0121: The call is ambiguous between the following methods or properties: 'Extensions1.extension(C1).operator checked --()' and 'Extensions2.extension(C1).operator --()' - // _ = --c1; - Diagnostic(ErrorCode.ERR_AmbigCall, "--").WithArguments("Extensions1.extension(C1).operator checked --()", "Extensions2.extension(C1).operator --()").WithLocation(36, 17) - ); -#else - // Ordering difference is acceptable and doesn't affect determinism. It is caused by ConditionallyDeOrder comp.VerifyEmitDiagnostics( // (32,13): error CS0121: The call is ambiguous between the following methods or properties: 'Extensions1.extension(C1).operator --()' and 'Extensions2.extension(C1).operator --()' // _ = --c1; @@ -7764,7 +7718,6 @@ static void Main() // _ = --c1; Diagnostic(ErrorCode.ERR_AmbigCall, "--").WithArguments("Extensions1.extension(C1).operator checked --()", "Extensions2.extension(C1).operator --()").WithLocation(36, 17) ); -#endif var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); @@ -10493,19 +10446,11 @@ static void Main() var comp = CreateCompilation(src, options: TestOptions.DebugExe); -#if DEBUG // Collection of extension blocks depends on GetTypeMembersUnordered for namespace, which conditionally de-orders types for DEBUG only. - comp.VerifyDiagnostics( - // (26,9): error CS0121: The call is ambiguous between the following methods or properties: 'Extensions2.extension(ref S2).operator ++()' and 'Extensions1.extension(ref S2).operator ++()' - // ++s2; - Diagnostic(ErrorCode.ERR_AmbigCall, "++").WithArguments("Extensions2.extension(ref S2).operator ++()", "Extensions1.extension(ref S2).operator ++()").WithLocation(26, 9) - ); -#else comp.VerifyDiagnostics( // (26,9): error CS0121: The call is ambiguous between the following methods or properties: 'Extensions1.extension(ref S2).operator ++()' and 'Extensions2.extension(ref S2).operator ++()' // ++s2; Diagnostic(ErrorCode.ERR_AmbigCall, "++").WithArguments("Extensions1.extension(ref S2).operator ++()", "Extensions2.extension(ref S2).operator ++()").WithLocation(26, 9) ); -#endif var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); @@ -10515,13 +10460,8 @@ static void Main() Assert.Null(symbolInfo.Symbol); Assert.Equal(CandidateReason.OverloadResolutionFailure, symbolInfo.CandidateReason); -#if DEBUG // Collection of extension blocks depends on GetTypeMembersUnordered for namespace, which conditionally de-orders types for DEBUG only. - AssertEx.Equal("Extensions2.extension(ref S2).operator ++()", symbolInfo.CandidateSymbols[0].ToDisplayString()); - AssertEx.Equal("Extensions1.extension(ref S2).operator ++()", symbolInfo.CandidateSymbols[1].ToDisplayString()); -#else AssertEx.Equal("Extensions1.extension(ref S2).operator ++()", symbolInfo.CandidateSymbols[0].ToDisplayString()); AssertEx.Equal("Extensions2.extension(ref S2).operator ++()", symbolInfo.CandidateSymbols[1].ToDisplayString()); -#endif } [Fact] @@ -10560,19 +10500,11 @@ static void Main() """ + CompilerFeatureRequiredAttribute; var comp = CreateCompilation(src, options: TestOptions.DebugExe); -#if DEBUG - comp.VerifyDiagnostics( - // (26,9): error CS9339: Operator resolution is ambiguous between the following members: 'Extensions2.extension(S2).operator ++(S2)' and 'Extensions1.extension(S2).operator ++(S2)' - // ++s2; - Diagnostic(ErrorCode.ERR_AmbigOperator, "++").WithArguments("Extensions2.extension(S2).operator ++(S2)", "Extensions1.extension(S2).operator ++(S2)").WithLocation(26, 9) - ); -#else comp.VerifyDiagnostics( // (26,9): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions1.extension(S2).operator ++(S2)' and 'Extensions2.extension(S2).operator ++(S2)' // ++s2; Diagnostic(ErrorCode.ERR_AmbigOperator, "++").WithArguments("Extensions1.extension(S2).operator ++(S2)", "Extensions2.extension(S2).operator ++(S2)").WithLocation(26, 9) ); -#endif var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); @@ -10582,13 +10514,8 @@ static void Main() Assert.Null(symbolInfo.Symbol); Assert.Equal(CandidateReason.Ambiguous, symbolInfo.CandidateReason); -#if DEBUG // Collection of extension blocks depends on GetTypeMembersUnordered for namespace, which conditionally de-orders types for DEBUG only. - AssertEx.Equal("Extensions2.extension(S2).operator ++(S2)", symbolInfo.CandidateSymbols[0].ToDisplayString()); - AssertEx.Equal("Extensions1.extension(S2).operator ++(S2)", symbolInfo.CandidateSymbols[1].ToDisplayString()); -#else AssertEx.Equal("Extensions1.extension(S2).operator ++(S2)", symbolInfo.CandidateSymbols[0].ToDisplayString()); AssertEx.Equal("Extensions2.extension(S2).operator ++(S2)", symbolInfo.CandidateSymbols[1].ToDisplayString()); -#endif } [Theory] @@ -13284,16 +13211,6 @@ static void Main() """; var comp = CreateCompilation(src, options: TestOptions.DebugExe); -#if DEBUG - comp.VerifyEmitDiagnostics( - // (35,16): error CS9339: Operator resolution is ambiguous between the following members:'Extensions2.extension(C1).operator -(C1, C1)' and 'Extensions1.extension(C1).operator -(C1, C1)' - // _ = c1 - c1; - Diagnostic(ErrorCode.ERR_AmbigOperator, "-").WithArguments("Extensions2.extension(C1).operator -(C1, C1)", "Extensions1.extension(C1).operator -(C1, C1)").WithLocation(35, 16), - // (39,20): error CS9339: Operator resolution is ambiguous between the following members:'Extensions1.extension(C1).operator checked -(C1, C1)' and 'Extensions2.extension(C1).operator -(C1, C1)' - // _ = c1 - c1; - Diagnostic(ErrorCode.ERR_AmbigOperator, "-").WithArguments("Extensions1.extension(C1).operator checked -(C1, C1)", "Extensions2.extension(C1).operator -(C1, C1)").WithLocation(39, 20) - ); -#else comp.VerifyEmitDiagnostics( // (35,16): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions1.extension(C1).operator -(C1, C1)' and 'Extensions2.extension(C1).operator -(C1, C1)' // _ = c1 - c1; @@ -13301,8 +13218,8 @@ static void Main() // (39,20): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions1.extension(C1).operator checked -(C1, C1)' and 'Extensions2.extension(C1).operator -(C1, C1)' // _ = c1 - c1; Diagnostic(ErrorCode.ERR_AmbigOperator, "-").WithArguments("Extensions1.extension(C1).operator checked -(C1, C1)", "Extensions2.extension(C1).operator -(C1, C1)").WithLocation(39, 20) - ); -#endif + ); + var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); var opNode = tree.GetRoot().DescendantNodes().OfType().Last(); @@ -23839,16 +23756,6 @@ static void Main() """; var comp = CreateCompilation(src, options: TestOptions.DebugExe); -#if DEBUG - comp.VerifyEmitDiagnostics( - // (35,16): error CS9339: Operator resolution is ambiguous between the following members: 'Extensions2.extension(C1).operator -(C1, C1)' and 'Extensions1.extension(C1).operator -(C1, C1)' - // _ = c1 -= c1; - Diagnostic(ErrorCode.ERR_AmbigOperator, "-=").WithArguments("Extensions2.extension(C1).operator -(C1, C1)", "Extensions1.extension(C1).operator -(C1, C1)").WithLocation(35, 16), - // (39,20): error CS9339: Operator resolution is ambiguous between the following members: 'Extensions1.extension(C1).operator checked -(C1, C1)' and 'Extensions2.extension(C1).operator -(C1, C1)' - // _ = c1 -= c1; - Diagnostic(ErrorCode.ERR_AmbigOperator, "-=").WithArguments("Extensions1.extension(C1).operator checked -(C1, C1)", "Extensions2.extension(C1).operator -(C1, C1)").WithLocation(39, 20) - ); -#else comp.VerifyEmitDiagnostics( // (35,16): error CS9342: Operator resolution is ambiguous between the following members: 'Extensions1.extension(C1).operator -(C1, C1)' and 'Extensions2.extension(C1).operator -(C1, C1)' // _ = c1 -= c1; @@ -23857,7 +23764,6 @@ static void Main() // _ = c1 -= c1; Diagnostic(ErrorCode.ERR_AmbigOperator, "-=").WithArguments("Extensions1.extension(C1).operator checked -(C1, C1)", "Extensions2.extension(C1).operator -(C1, C1)").WithLocation(39, 20) ); -#endif var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); @@ -23920,17 +23826,6 @@ static void Main() var comp = CreateCompilation([src, CompilerFeatureRequiredAttribute], options: TestOptions.DebugExe); -#if DEBUG // Collection of extension blocks depends on GetTypeMembersUnordered for namespace, which conditionally de-orders types for DEBUG only. - comp.VerifyEmitDiagnostics( - // (35,16): error CS0121: The call is ambiguous between the following methods or properties: 'Extensions2.extension(C1).operator -=(C1)' and 'Extensions1.extension(C1).operator -=(C1)' - // _ = c1 -= c1; - Diagnostic(ErrorCode.ERR_AmbigCall, "-=").WithArguments("Extensions2.extension(C1).operator -=(C1)", "Extensions1.extension(C1).operator -=(C1)").WithLocation(35, 16), - // (39,20): error CS0121: The call is ambiguous between the following methods or properties: 'Extensions1.extension(C1).operator checked -=(C1)' and 'Extensions2.extension(C1).operator -=(C1)' - // _ = c1 -= c1; - Diagnostic(ErrorCode.ERR_AmbigCall, "-=").WithArguments("Extensions1.extension(C1).operator checked -=(C1)", "Extensions2.extension(C1).operator -=(C1)").WithLocation(39, 20) - ); -#else - // Ordering difference is acceptable and doesn't affect determinism. It is caused by ConditionallyDeOrder comp.VerifyEmitDiagnostics( // (35,16): error CS0121: The call is ambiguous between the following methods or properties: 'Extensions1.extension(C1).operator -=(C1)' and 'Extensions2.extension(C1).operator -=(C1)' // _ = c1 -= c1; @@ -23939,7 +23834,6 @@ static void Main() // _ = c1 -= c1; Diagnostic(ErrorCode.ERR_AmbigCall, "-=").WithArguments("Extensions1.extension(C1).operator checked -=(C1)", "Extensions2.extension(C1).operator -=(C1)").WithLocation(39, 20) ); -#endif var tree = comp.SyntaxTrees.First(); var model = comp.GetSemanticModel(tree); diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs index d638212f5db5..15d9d9102a1c 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests.cs @@ -2761,7 +2761,7 @@ enum E { } Assert.False(symbol.IsExtension); } - [Fact] + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82444")] public void Attributes_01() { var src = """ @@ -2773,15 +2773,15 @@ public static class Extensions """; var comp = CreateCompilation(src); comp.VerifyEmitDiagnostics( - // (3,6): error CS0592: Attribute 'System.Obsolete' is not valid on this declaration type. It is only valid on 'class, struct, enum, constructor, method, property, indexer, field, event, interface, delegate' declarations. + // (3,5): error CS7014: Attributes are not valid in this context. // [System.Obsolete] - Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "System.Obsolete").WithArguments("System.Obsolete", "class, struct, enum, constructor, method, property, indexer, field, event, interface, delegate").WithLocation(3, 6)); + Diagnostic(ErrorCode.ERR_AttributesNotAllowed, "[System.Obsolete]").WithLocation(3, 5)); var tree = comp.SyntaxTrees[0]; var model = comp.GetSemanticModel(tree); var type = tree.GetRoot().DescendantNodes().OfType().Single(); var symbol = model.GetDeclaredSymbol(type); - AssertEx.SetEqual(["System.ObsoleteAttribute"], symbol.GetAttributes().Select(a => a.ToString())); + Assert.Empty(symbol.GetAttributes()); } [Fact] @@ -2801,13 +2801,278 @@ public MyAttribute(string s) { } """; var comp = CreateCompilation(src); comp.VerifyEmitDiagnostics( - // (3,6): error CS0592: Attribute 'My' is not valid on this declaration type. It is only valid on 'assembly, module, class, struct, enum, constructor, method, property, indexer, field, event, interface, parameter, delegate, return, type parameter' declarations. - // [My(nameof(o)), My(nameof(Extensions))] - Diagnostic(ErrorCode.ERR_AttributeOnBadSymbolType, "My").WithArguments("My", "assembly, module, class, struct, enum, constructor, method, property, indexer, field, event, interface, parameter, delegate, return, type parameter").WithLocation(3, 6), - // (3,21): error CS0579: Duplicate 'My' attribute + // (3,5): error CS7014: Attributes are not valid in this context. // [My(nameof(o)), My(nameof(Extensions))] - Diagnostic(ErrorCode.ERR_DuplicateAttribute, "My").WithArguments("My").WithLocation(3, 21) - ); + Diagnostic(ErrorCode.ERR_AttributesNotAllowed, "[My(nameof(o)), My(nameof(Extensions))]").WithLocation(3, 5)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82445")] + public void Attributes_03() + { + var src = """ +public static class E +{ + [return: System.Obsolete] + extension(object) { } +} +"""; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics( + // (3,5): error CS7014: Attributes are not valid in this context. + // [return: System.Obsolete] + Diagnostic(ErrorCode.ERR_AttributesNotAllowed, "[return: System.Obsolete]").WithLocation(3, 5)); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var type = tree.GetRoot().DescendantNodes().OfType().Single(); + var symbol = model.GetDeclaredSymbol(type); + Assert.Empty(symbol.GetAttributes()); + } + + [Theory, WorkItem("https://github.com/dotnet/roslyn/issues/82445")] + [InlineData("return")] + [InlineData("assembly")] + [InlineData("module")] + [InlineData("type")] + [InlineData("method")] + [InlineData("field")] + [InlineData("property")] + [InlineData("typevar")] + public void Attributes_04(string target) + { + var src = $$""" +public static class E +{ + [{{target}}: My] + extension(object o) { } +} + +public class MyAttribute : System.Attribute { } +"""; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics( + // (3,5): error CS7014: Attributes are not valid in this context. + // [assembly: My] + Diagnostic(ErrorCode.ERR_AttributesNotAllowed, $"[{target}: My]").WithLocation(3, 5)); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var type = tree.GetRoot().DescendantNodes().OfType().Single(); + var symbol = model.GetDeclaredSymbol(type); + Assert.Empty(symbol.GetAttributes()); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82445")] + public void Attributes_05() + { + var src = """ +public static class E +{ + extension<[typevar: My] T>(object o) { } +} + +public class MyAttribute : System.Attribute { } +"""; + var comp = CreateCompilation(src); + var verifier = CompileAndVerify(comp).VerifyDiagnostics(); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var type = tree.GetRoot().DescendantNodes().OfType().Single(); + var symbol = model.GetDeclaredSymbol(type); + AssertEx.SetEqual(["MyAttribute"], symbol.TypeParameters[0].GetAttributes().Select(a => a.ToString())); + + verifier.VerifyTypeIL("E", """ +.class public auto ansi abstract sealed beforefieldinit E + extends [mscorlib]System.Object +{ + .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Nested Types + .class nested public auto ansi sealed specialname '$F3EC63F55CD2663D3F6B00F6D7E0AC7E`1'<$T0> + extends [mscorlib]System.Object + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Nested Types + .class nested public auto ansi abstract sealed specialname '$6BF82573E4B1538004B57C462825CE6A' + extends [mscorlib]System.Object + { + .param type T + .custom instance void MyAttribute::.ctor() = ( + 01 00 00 00 + ) + // Methods + .method private hidebysig specialname static + void '$' ( + object o + ) cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x2067 + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method '$6BF82573E4B1538004B57C462825CE6A'::'$' + } // end of class $6BF82573E4B1538004B57C462825CE6A + } // end of class $F3EC63F55CD2663D3F6B00F6D7E0AC7E`1 +} // end of class E +""".Replace("[mscorlib]", ExecutionConditionUtil.IsMonoOrCoreClr ? "[netstandard]" : "[mscorlib]")); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82445")] + public void Attributes_06() + { + var src = """ +public static class Extensions +{ + [typevar: My] + extension(object o) { } +} + +public class MyAttribute : System.Attribute { } +"""; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics( + // (3,5): error CS7014: Attributes are not valid in this context. + // [typevar: My] + Diagnostic(ErrorCode.ERR_AttributesNotAllowed, "[typevar: My]").WithLocation(3, 5)); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var type = tree.GetRoot().DescendantNodes().OfType().Single(); + var symbol = model.GetDeclaredSymbol(type); + Assert.Empty(symbol.GetAttributes()); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82445")] + public void Attributes_07() + { + var src = """ +public static class Extensions +{ + extension([param: My] object o) { } +} + +public class MyAttribute : System.Attribute { } +"""; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics(); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var type = tree.GetRoot().DescendantNodes().OfType().Single(); + var symbol = model.GetDeclaredSymbol(type); + AssertEx.SetEqual(["MyAttribute"], symbol.ExtensionParameter.GetAttributes().Select(a => a.ToString())); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82445")] + public void Attributes_08() + { + var src = $$""" +public static class E +{ + [fake: My] + extension(object o) { } +} + +public class MyAttribute : System.Attribute { } +"""; + var comp = CreateCompilation(src); + comp.VerifyEmitDiagnostics( + // (3,5): error CS7014: Attributes are not valid in this context. + // [fake: My] + Diagnostic(ErrorCode.ERR_AttributesNotAllowed, "[fake: My]").WithLocation(3, 5)); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var type = tree.GetRoot().DescendantNodes().OfType().Single(); + var symbol = model.GetDeclaredSymbol(type); + Assert.Empty(symbol.GetAttributes()); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82445")] + public void Attributes_09() + { + var src = """ +public static class E +{ + extension<[typevar: A] T>(object o) { } + extension<[typevar: B] U>(object o) { } +} + +public class AAttribute : System.Attribute { } +public class BAttribute : System.Attribute { } +"""; + var comp = CreateCompilation(src); + var verifier = CompileAndVerify(comp).VerifyDiagnostics(); + + verifier.VerifyTypeIL("E", """ +.class public auto ansi abstract sealed beforefieldinit E + extends [mscorlib]System.Object +{ + .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Nested Types + .class nested public auto ansi sealed specialname '$F3EC63F55CD2663D3F6B00F6D7E0AC7E`1'<$T0> + extends [mscorlib]System.Object + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Nested Types + .class nested public auto ansi abstract sealed specialname '$79443493B28002B8C849C38A5107CBE5' + extends [mscorlib]System.Object + { + .param type T + .custom instance void AAttribute::.ctor() = ( + 01 00 00 00 + ) + // Methods + .method private hidebysig specialname static + void '$' ( + object o + ) cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x2067 + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method '$79443493B28002B8C849C38A5107CBE5'::'$' + } // end of class $79443493B28002B8C849C38A5107CBE5 + .class nested public auto ansi abstract sealed specialname '$892BEE6C9B63CFFC80DE2A9BC76F34AB' + extends [mscorlib]System.Object + { + .param type U + .custom instance void BAttribute::.ctor() = ( + 01 00 00 00 + ) + // Methods + .method private hidebysig specialname static + void '$' ( + object o + ) cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x2067 + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method '$892BEE6C9B63CFFC80DE2A9BC76F34AB'::'$' + } // end of class $892BEE6C9B63CFFC80DE2A9BC76F34AB + } // end of class $F3EC63F55CD2663D3F6B00F6D7E0AC7E`1 +} // end of class E +""".Replace("[mscorlib]", ExecutionConditionUtil.IsMonoOrCoreClr ? "[netstandard]" : "[mscorlib]")); } [Fact] @@ -5457,7 +5722,7 @@ static void verifySymbols(ModuleSymbol m) Assert.False(implementation.HasSpecialName); Assert.False(implementation.HasRuntimeSpecialName); - Assert.True(implementation.ContainingType.MightContainExtensionMethods); + Assert.True(implementation.ContainingType.MightContainExtensions); Assert.Contains("M", extensions.MemberNames); Assert.NotEmpty(extensions.GetSimpleNonTypeMembers("M")); @@ -8527,7 +8792,7 @@ static void verifySymbols(ModuleSymbol m) Assert.False(implementation.HasSpecialName); Assert.False(implementation.HasRuntimeSpecialName); - Assert.True(implementation.ContainingType.MightContainExtensionMethods); + Assert.True(implementation.ContainingType.MightContainExtensions); if (m is PEModuleSymbol peModuleSymbol) { @@ -38805,12 +39070,12 @@ static void F() { } var model = comp.GetSemanticModel(tree); var memberAccess = GetSyntaxes(tree, "a.F").ToArray(); Assert.Null(model.GetSymbolInfo(memberAccess[0]).Symbol); - AssertEx.SequenceEqual(["void A.F()"], model.GetSymbolInfo(memberAccess[0]).CandidateSymbols.ToTestDisplayStrings()); - AssertEx.SequenceEqual(["void A.F()", "void E2.$8048A6C8BE30A622530249B904B537EB.F()", "void A.F()"], model.GetMemberGroup(memberAccess[0]).ToTestDisplayStrings()); + AssertEqualAndNoDuplicates(["void A.F()"], model.GetSymbolInfo(memberAccess[0]).CandidateSymbols.ToTestDisplayStrings()); + AssertEqualAndNoDuplicates(["void A.F()", "void E2.$8048A6C8BE30A622530249B904B537EB.F()", "void A.F()"], model.GetMemberGroup(memberAccess[0]).ToTestDisplayStrings()); Assert.Null(model.GetSymbolInfo(memberAccess[1]).Symbol); - AssertEx.SequenceEqual(["void A.F()", "void E2.$8048A6C8BE30A622530249B904B537EB.F()", "void A.F()"], model.GetSymbolInfo(memberAccess[1]).CandidateSymbols.ToTestDisplayStrings()); - AssertEx.SequenceEqual(["void A.F()", "void E2.$8048A6C8BE30A622530249B904B537EB.F()", "void A.F()"], model.GetMemberGroup(memberAccess[1]).ToTestDisplayStrings()); + AssertEqualAndNoDuplicates(["void A.F()", "void E2.$8048A6C8BE30A622530249B904B537EB.F()", "void A.F()"], model.GetSymbolInfo(memberAccess[1]).CandidateSymbols.ToTestDisplayStrings()); + AssertEqualAndNoDuplicates(["void A.F()", "void E2.$8048A6C8BE30A622530249B904B537EB.F()", "void A.F()"], model.GetMemberGroup(memberAccess[1]).ToTestDisplayStrings()); } [Fact] diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests2.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests2.cs index 95bddac0a7b4..5040e55b4b6d 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests2.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/ExtensionTests2.cs @@ -3195,8 +3195,8 @@ public class Derived : Base { } Diagnostic(ErrorCode.WRN_NullAsNonNullable, "(Derived?)null").WithLocation(9, 16)); } - [Fact] - public void Nullability_NullableContext_01() + [Theory, CombinatorialData] + public void Nullability_NullableContext_01(bool useCompilationReference) { var src = """ #nullable enable @@ -3227,10 +3227,27 @@ static void validate(ModuleSymbol m) AssertEx.SetEqual(m is SourceModuleSymbol ? new string[] { } : ["System.Runtime.CompilerServices.NullableContextAttribute(1)", "System.Runtime.CompilerServices.NullableAttribute(0)"], m.GlobalNamespace.GetTypeMember("E").GetAttributes().ToStrings()); } + + var src2 = """ +#nullable enable + +object.M1(null); +object.M1(new object()); +E.M1(null); +E.M1(new object()); +"""; + var comp2 = CreateCompilation(src2, references: [AsReference(comp, useCompilationReference)], targetFramework: TargetFramework.Net90); + comp2.VerifyEmitDiagnostics( + // (3,11): warning CS8625: Cannot convert null literal to non-nullable reference type. + // object.M1(null); + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(3, 11), + // (5,6): warning CS8625: Cannot convert null literal to non-nullable reference type. + // E.M1(null); + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(5, 6)); } - [Fact] - public void Nullability_NullableContext_02() + [Theory, CombinatorialData] + public void Nullability_NullableContext_02(bool useCompilationReference) { var src = """ #nullable enable @@ -3261,6 +3278,509 @@ static void validate(ModuleSymbol m) AssertEx.SetEqual(m is SourceModuleSymbol ? new string[] { } : ["System.Runtime.CompilerServices.NullableContextAttribute(2)", "System.Runtime.CompilerServices.NullableAttribute(0)"], m.GlobalNamespace.GetTypeMember("E").GetAttributes().ToStrings()); } + + var src2 = """ +#nullable enable + +object.M1(null); +object.M1(new object()); +E.M1(null); +E.M1(new object()); +"""; + var comp2 = CreateCompilation(src2, references: [AsReference(comp, useCompilationReference)], targetFramework: TargetFramework.Net90); + comp2.VerifyEmitDiagnostics(); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81654")] + public void Nullability_NullableContext_03(bool useCompilationReference) + { + var libSrc = """ +#nullable enable + +public static class E +{ + public static void X1(object o) { } + public static void X2(object o) { } + public static void X3(object o) { } + public static void X4(object o) { } + public static void X5(object o) { } + public static void X6(object o) { } + extension(int i) + { + public void M2() { } + } +} +"""; + + var libComp = CreateCompilation(libSrc, targetFramework: TargetFramework.Net100); + var verifier = CompileAndVerify(libComp, symbolValidator: validate, verify: Verification.Skipped); + verifier.VerifyDiagnostics(); + + static void validate(ModuleSymbol m) + { + var e = m.GlobalNamespace.GetTypeMember("E"); + + AssertEx.SetEqual([ + "System.Runtime.CompilerServices.NullableContextAttribute(1)", + "System.Runtime.CompilerServices.NullableAttribute(0)" + ], e.GetAttributes().ToStrings()); + + var extensionMethod = e.GetTypeMembers().Single().GetMember("M2"); + AssertEx.Equal(["System.Runtime.CompilerServices.NullableContextAttribute(2)"], extensionMethod.GetAttributes().ToStrings()); + var implementationMethod = e.GetMember("M2"); + AssertEx.Equal(["System.Runtime.CompilerServices.NullableContextAttribute(2)"], implementationMethod.GetAttributes().ToStrings()); + } + + var src = """ +#nullable enable + +class C +{ + void Test() + { + int i = 42; + i.M2(); + E.M2(i); + } +} +"""; + + var comp = CreateCompilation(src, references: [AsReference(libComp, useCompilationReference)], targetFramework: TargetFramework.Net100); + comp.VerifyEmitDiagnostics(); + + var extensionMethod = comp.GlobalNamespace.GetMember("E").GetTypeMembers().Single().GetMember("M2"); + Assert.True(extensionMethod.IsExtensionBlockMember()); + var extensionTypeParam = extensionMethod.TypeParameters.Single(); + Assert.False(extensionTypeParam.HasNotNullConstraint); + + var implementationMethod = comp.GetMember("E.M2"); + var implementationTypeParam = implementationMethod.TypeParameters.Single(); + Assert.False(implementationTypeParam.HasNotNullConstraint); + + verifier.VerifyTypeIL("E", """ +.class public auto ansi abstract sealed beforefieldinit E + extends [System.Runtime]System.Object +{ + .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( + 01 00 01 00 00 + ) + .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( + 01 00 00 00 00 + ) + .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Nested Types + .class nested public auto ansi sealed specialname '$BA41CFE2B5EDAEB8C1B9062F59ED4D69' + extends [System.Runtime]System.Object + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Nested Types + .class nested public auto ansi abstract sealed specialname '$F4B4FFE41AB49E80A4ECF390CF6EB372' + extends [System.Runtime]System.Object + { + // Methods + .method public hidebysig specialname static + void '$' ( + int32 i + ) cil managed + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method '$F4B4FFE41AB49E80A4ECF390CF6EB372'::'$' + } // end of class $F4B4FFE41AB49E80A4ECF390CF6EB372 + // Methods + .method public hidebysig + instance void M2 () cil managed + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( + 01 00 02 00 00 + ) + .custom instance void System.Runtime.CompilerServices.ExtensionMarkerAttribute::.ctor(string) = ( + 01 00 24 3c 4d 3e 24 46 34 42 34 46 46 45 34 31 + 41 42 34 39 45 38 30 41 34 45 43 46 33 39 30 43 + 46 36 45 42 33 37 32 00 00 + ) + // Method begins at RVA 0x2071 + // Code size 6 (0x6) + .maxstack 8 + IL_0000: newobj instance void [System.Runtime]System.NotSupportedException::.ctor() + IL_0005: throw + } // end of method '$BA41CFE2B5EDAEB8C1B9062F59ED4D69'::M2 + } // end of class $BA41CFE2B5EDAEB8C1B9062F59ED4D69 + // Methods + .method public hidebysig static + void X1 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X1 + .method public hidebysig static + void X2 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X2 + .method public hidebysig static + void X3 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X3 + .method public hidebysig static + void X4 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X4 + .method public hidebysig static + void X5 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X5 + .method public hidebysig static + void X6 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X6 + .method public hidebysig static + void M2 ( + int32 i + ) cil managed + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( + 01 00 02 00 00 + ) + .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::M2 +} // end of class E +"""); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81654")] + public void Nullability_NullableContext_04(bool useCompilationReference) + { + var libSrc = """ +#nullable enable + +public static class E +{ + public static void X1(object? o) { } + public static void X2(object? o) { } + public static void X3(object? o) { } + public static void X4(object? o) { } + public static void X5(object? o) { } + public static void X6(object? o) { } + extension(int i) + { + public void M2() where T : notnull { } + } +} +"""; + + var libComp = CreateCompilation(libSrc, targetFramework: TargetFramework.Net100); + var verifier = CompileAndVerify(libComp, symbolValidator: validate, verify: Verification.Skipped); + verifier.VerifyDiagnostics(); + + static void validate(ModuleSymbol m) + { + var e = m.GlobalNamespace.GetTypeMember("E"); + + AssertEx.SetEqual([ + "System.Runtime.CompilerServices.NullableContextAttribute(2)", + "System.Runtime.CompilerServices.NullableAttribute(0)" + ], e.GetAttributes().ToStrings()); + + var extensionMethod = e.GetTypeMembers().Single().GetMember("M2"); + AssertEx.SetEqual(["System.Runtime.CompilerServices.NullableContextAttribute(1)"], extensionMethod.GetAttributes().ToStrings()); + var implementationMethod = e.GetMember("M2"); + AssertEx.SetEqual(["System.Runtime.CompilerServices.NullableContextAttribute(1)"], implementationMethod.GetAttributes().ToStrings()); + } + + var src = """ +#nullable enable + +class C +{ + void Test() + { + int i = 42; + i.M2(); + E.M2(i); + } +} +"""; + + var comp = CreateCompilation(src, references: [AsReference(libComp, useCompilationReference)], targetFramework: TargetFramework.Net100); + + var extensionMethod = comp.GlobalNamespace.GetMember("E").GetTypeMembers().Single().GetMember("M2"); + Assert.True(extensionMethod.IsExtensionBlockMember()); + var extensionTypeParam = extensionMethod.TypeParameters.Single(); + + var implementationMethod = comp.GetMember("E.M2"); + var implementationTypeParam = implementationMethod.TypeParameters.Single(); + + Assert.True(extensionTypeParam.HasNotNullConstraint); + Assert.True(implementationTypeParam.HasNotNullConstraint); + + comp.VerifyEmitDiagnostics( + // (8,9): warning CS8714: The type 'string?' cannot be used as type parameter 'T' in the generic type or method 'E.extension(int).M2()'. Nullability of type argument 'string?' doesn't match 'notnull' constraint. + // i.M2(); + Diagnostic(ErrorCode.WRN_NullabilityMismatchInTypeParameterNotNullConstraint, "i.M2").WithArguments("E.extension(int).M2()", "T", "string?").WithLocation(8, 9), + // (9,9): warning CS8714: The type 'string?' cannot be used as type parameter 'T' in the generic type or method 'E.M2(int)'. Nullability of type argument 'string?' doesn't match 'notnull' constraint. + // E.M2(i); + Diagnostic(ErrorCode.WRN_NullabilityMismatchInTypeParameterNotNullConstraint, "E.M2").WithArguments("E.M2(int)", "T", "string?").WithLocation(9, 9)); + + verifier.VerifyTypeIL("E", """ +.class public auto ansi abstract sealed beforefieldinit E + extends [System.Runtime]System.Object +{ + .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( + 01 00 02 00 00 + ) + .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8) = ( + 01 00 00 00 00 + ) + .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Nested Types + .class nested public auto ansi sealed specialname '$BA41CFE2B5EDAEB8C1B9062F59ED4D69' + extends [System.Runtime]System.Object + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Nested Types + .class nested public auto ansi abstract sealed specialname '$F4B4FFE41AB49E80A4ECF390CF6EB372' + extends [System.Runtime]System.Object + { + // Methods + .method public hidebysig specialname static + void '$' ( + int32 i + ) cil managed + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method '$F4B4FFE41AB49E80A4ECF390CF6EB372'::'$' + } // end of class $F4B4FFE41AB49E80A4ECF390CF6EB372 + // Methods + .method public hidebysig + instance void M2 () cil managed + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( + 01 00 01 00 00 + ) + .custom instance void System.Runtime.CompilerServices.ExtensionMarkerAttribute::.ctor(string) = ( + 01 00 24 3c 4d 3e 24 46 34 42 34 46 46 45 34 31 + 41 42 34 39 45 38 30 41 34 45 43 46 33 39 30 43 + 46 36 45 42 33 37 32 00 00 + ) + // Method begins at RVA 0x2071 + // Code size 6 (0x6) + .maxstack 8 + IL_0000: newobj instance void [System.Runtime]System.NotSupportedException::.ctor() + IL_0005: throw + } // end of method '$BA41CFE2B5EDAEB8C1B9062F59ED4D69'::M2 + } // end of class $BA41CFE2B5EDAEB8C1B9062F59ED4D69 + // Methods + .method public hidebysig static + void X1 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X1 + .method public hidebysig static + void X2 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X2 + .method public hidebysig static + void X3 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X3 + .method public hidebysig static + void X4 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X4 + .method public hidebysig static + void X5 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X5 + .method public hidebysig static + void X6 ( + object o + ) cil managed + { + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::X6 + .method public hidebysig static + void M2 ( + int32 i + ) cil managed + { + .custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = ( + 01 00 01 00 00 + ) + .custom instance void [System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x206f + // Code size 1 (0x1) + .maxstack 8 + IL_0000: ret + } // end of method E::M2 +} // end of class E +"""); + } + + [Theory, CombinatorialData] + public void Nullability_NullableContext_05(bool useCompilationReference) + { + var libSrc = """ +#nullable enable + +public static class E +{ + public static object? X1() => throw null!; + public static object? X2() => throw null!; + public static object? X3() => throw null!; + public static object? X4() => throw null!; + public static object? X5() => throw null!; + public static object? X6() => throw null!; + extension(object) + { + public static object M() => throw null!; + } +} +"""; + + var libComp = CreateCompilation(libSrc, targetFramework: TargetFramework.Net100).VerifyEmitDiagnostics(); + + var src = """ +#nullable enable + +object.M().ToString();; +E.M().ToString(); +"""; + + var comp = CreateCompilation(src, references: [AsReference(libComp, useCompilationReference)], targetFramework: TargetFramework.Net100); + comp.VerifyEmitDiagnostics(); + } + + [Theory, CombinatorialData] + public void Nullability_NullableContext_06(bool useCompilationReference) + { + var libSrc = """ +#nullable enable + +public static class E +{ + public static object X1() => throw null!; + public static object X2() => throw null!; + public static object X3() => throw null!; + public static object X4() => throw null!; + public static object X5() => throw null!; + public static object X6() => throw null!; + extension(object) + { + public static object? M() => throw null!; + } +} +"""; + + var libComp = CreateCompilation(libSrc, targetFramework: TargetFramework.Net100).VerifyEmitDiagnostics(); + + var src = """ +#nullable enable + +object.M().ToString(); +E.M().ToString(); +"""; + + var comp = CreateCompilation(src, references: [AsReference(libComp, useCompilationReference)], targetFramework: TargetFramework.Net100); + comp.VerifyEmitDiagnostics( + // (3,1): warning CS8602: Dereference of a possibly null reference. + // object.M().ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "object.M()").WithLocation(3, 1), + // (4,1): warning CS8602: Dereference of a possibly null reference. + // E.M().ToString(); + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "E.M()").WithLocation(4, 1)); } [Fact] @@ -38279,5 +38799,66 @@ public static class E // System.Nullable>.P = 0; Diagnostic(ErrorCode.WRN_NullabilityMismatchInArgument, "System.Nullable>").WithArguments("(object, object)?", "(object?, object?)?", "t", "E.extension((object?, object?)?)").WithLocation(4, 1)); } + + [Fact] + public void Script_01() + { + var source = """ +static class E +{ + extension(object o) + { + static void F() { } + } } +class C +{ + void M() { this.F(); } +} + +new object().F(); +"""; + CreateCompilation(source, parseOptions: TestOptions.Script).VerifyEmitDiagnostics( + // (3,5): error CS9283: Extensions must be declared in a top-level, non-generic, static class + // extension(object o) + Diagnostic(ErrorCode.ERR_BadExtensionContainingType, "extension").WithLocation(3, 5), + // (11,21): error CS1061: 'C' does not contain a definition for 'F' and no accessible extension method 'F' accepting a first argument of type 'C' could be found (are you missing a using directive or an assembly reference?) + // void M() { this.F(); } + Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "F").WithArguments("C", "F").WithLocation(11, 21), + // (14,14): error CS1061: 'object' does not contain a definition for 'F' and no accessible extension method 'F' accepting a first argument of type 'object' could be found (are you missing a using directive or an assembly reference?) + // new object().F(); + Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "F").WithArguments("object", "F").WithLocation(14, 14)); + } + + [Fact] + public void Script_02() + { + var source = """ +static class E +{ + extension(object o) + { + static int Property => 42; + } +} + +class C +{ + void M() { this.F(); } +} + +_ = new object().Property; +"""; + CreateCompilation(source, parseOptions: TestOptions.Script).VerifyEmitDiagnostics( + // (3,5): error CS9283: Extensions must be declared in a top-level, non-generic, static class + // extension(object o) + Diagnostic(ErrorCode.ERR_BadExtensionContainingType, "extension").WithLocation(3, 5), + // (11,21): error CS1061: 'C' does not contain a definition for 'F' and no accessible extension method 'F' accepting a first argument of type 'C' could be found (are you missing a using directive or an assembly reference?) + // void M() { this.F(); } + Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "F").WithArguments("C", "F").WithLocation(11, 21), + // (14,18): error CS1061: 'object' does not contain a definition for 'Property' and no accessible extension method 'Property' accepting a first argument of type 'object' could be found (are you missing a using directive or an assembly reference?) + // _ = new object().Property; + Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "Property").WithArguments("object", "Property").WithLocation(14, 18)); + } +} diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/InlineArrayTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/InlineArrayTests.cs index bbd96c4c04f4..24cee460e5cb 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/InlineArrayTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/InlineArrayTests.cs @@ -7011,6 +7011,7 @@ static void Main() " + Buffer10Definition; var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + comp.MakeMemberMissing(WellKnownMember.System_Span_T__Slice_Int); var verifier = CompileAndVerify(comp, expectedOutput: "0 9 111", verify: Verification.Fails).VerifyDiagnostics(); verifier.VerifyIL("Program.M2", @@ -7030,6 +7031,28 @@ .locals init (System.Span V_0) IL_0013: call ""System.Span System.Span.Slice(int, int)"" IL_0018: ret } +"); + + comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + comp.MakeMemberMissing(WellKnownMember.System_ReadOnlySpan_T__Slice_Int); + verifier = CompileAndVerify(comp, expectedOutput: "0 9 111", verify: Verification.Fails).VerifyDiagnostics(); + + verifier.VerifyIL("Program.M2", +@" +{ + // Code size 23 (0x17) + .maxstack 2 + .locals init (System.Span V_0) + IL_0000: ldarg.0 + IL_0001: ldflda ""Buffer10 C.F"" + IL_0006: ldc.i4.s 10 + IL_0008: call ""System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)"" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: ldc.i4.1 + IL_0011: call ""System.Span System.Span.Slice(int)"" + IL_0016: ret +} "); } @@ -7061,6 +7084,7 @@ static void Main() " + Buffer10Definition; var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + comp.MakeMemberMissing(WellKnownMember.System_Span_T__Slice_Int); var verifier = CompileAndVerify(comp, expectedOutput: "0 10 111", verify: Verification.Fails).VerifyDiagnostics(); verifier.VerifyIL("Program.M2", @@ -7085,6 +7109,27 @@ .locals init (int V_0, IL_0017: call ""System.Span System.Span.Slice(int, int)"" IL_001c: ret } +"); + + comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + verifier = CompileAndVerify(comp, expectedOutput: "0 10 111", verify: Verification.Fails).VerifyDiagnostics(); + + verifier.VerifyIL("Program.M2", +@" +{ + // Code size 23 (0x17) + .maxstack 2 + .locals init (System.Span V_0) + IL_0000: ldarg.0 + IL_0001: ldflda ""Buffer10 C.F"" + IL_0006: ldc.i4.s 10 + IL_0008: call ""System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)"" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: ldarg.1 + IL_0011: call ""System.Span System.Span.Slice(int)"" + IL_0016: ret +} "); } @@ -23633,5 +23678,523 @@ static void Test(Buffer4 x) // nx?[2] = 3; // 1 Diagnostic(ErrorCode.ERR_AssgLvalueExpected, "[2]").WithLocation(8, 12)); } + + [ConditionalTheory(typeof(CoreClrOnly))] + [CombinatorialData] + public void SliceStart_01(bool isMissing) + { + // slice with open-ended range producing Span + var src = """ +class C +{ + public Buffer10 F; +} + +class Program +{ + static void Main() + { + var x = new C(); + M(x)[0] = 111; + System.Console.Write(M(x).Length); + System.Console.Write(' '); + System.Console.Write(x.F[1]); + } + + static System.Span M(C x) => x.F[1..]; +} +""" + Buffer10Definition; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + if (isMissing) + comp.MakeMemberMissing(WellKnownMember.System_Span_T__Slice_Int); + + var verifier = CompileAndVerify(comp, expectedOutput: "9 111", verify: Verification.Fails).VerifyDiagnostics(); + + verifier.VerifyIL("Program.M", + isMissing ? """ +{ + // Code size 25 (0x19) + .maxstack 3 + .locals init (System.Span V_0) + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: ldc.i4.s 10 + IL_0008: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: ldc.i4.1 + IL_0011: ldc.i4.s 9 + IL_0013: call "System.Span System.Span.Slice(int, int)" + IL_0018: ret +} +""" : """ +{ + // Code size 23 (0x17) + .maxstack 2 + .locals init (System.Span V_0) + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: ldc.i4.s 10 + IL_0008: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: ldc.i4.1 + IL_0011: call "System.Span System.Span.Slice(int)" + IL_0016: ret +} +"""); + } + + [ConditionalTheory(typeof(CoreClrOnly))] + [InlineData(null)] + [InlineData("Span")] + [InlineData("ReadOnlySpan")] + public void SliceStart_02(string missingSliceInt) + { + // slice with open-ended range producing ReadOnlySpan + var src = """ +class C +{ + public Buffer10 F; +} + +class Program +{ + static void Main() + { + var x = new C(); + System.Console.Write(M(x).Length); + } + + static System.ReadOnlySpan M(C x) => x.F[1..]; +} +""" + Buffer10Definition; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + if (missingSliceInt == "Span") + comp.MakeMemberMissing(WellKnownMember.System_Span_T__Slice_Int); + else if (missingSliceInt == "ReadOnlySpan") + comp.MakeMemberMissing(WellKnownMember.System_ReadOnlySpan_T__Slice_Int); + + var verifier = CompileAndVerify(comp, expectedOutput: "9", verify: Verification.Fails).VerifyDiagnostics(); + + verifier.VerifyIL("Program.M", + missingSliceInt == "Span" ? @" +{ + // Code size 30 (0x1e) + .maxstack 3 + .locals init (System.Span V_0) + IL_0000: ldarg.0 + IL_0001: ldflda ""Buffer10 C.F"" + IL_0006: ldc.i4.s 10 + IL_0008: call ""System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)"" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: ldc.i4.1 + IL_0011: ldc.i4.s 9 + IL_0013: call ""System.Span System.Span.Slice(int, int)"" + IL_0018: call ""System.ReadOnlySpan System.Span.op_Implicit(System.Span)"" + IL_001d: ret +} +" : @" +{ + // Code size 28 (0x1c) + .maxstack 2 + .locals init (System.Span V_0) + IL_0000: ldarg.0 + IL_0001: ldflda ""Buffer10 C.F"" + IL_0006: ldc.i4.s 10 + IL_0008: call ""System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)"" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: ldc.i4.1 + IL_0011: call ""System.Span System.Span.Slice(int)"" + IL_0016: call ""System.ReadOnlySpan System.Span.op_Implicit(System.Span)"" + IL_001b: ret +} +"); + } + + [ConditionalTheory(typeof(CoreClrOnly))] + [CombinatorialData] + public void SliceStart_03(bool isMissing) + { + // slice with `GetStart()..` + var src = """ +class C +{ + public Buffer10 F; +} + +class Program +{ + static void Main() + { + var x = new C(); + M2(x)[0] = 111; + System.Console.Write(M2(x).Length); + System.Console.Write(' '); + System.Console.Write(x.F[1]); + } + + static System.Span M2(C x) => x.F[GetStart()..]; + static System.Index GetStart() { System.Console.Write("GetStart "); return ^9; } +} +""" + Buffer10Definition; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + if (isMissing) + comp.MakeMemberMissing(WellKnownMember.System_Span_T__Slice_Int); + + var verifier = CompileAndVerify(comp, expectedOutput: "GetStart GetStart 9 111", verify: Verification.Fails).VerifyDiagnostics(); + + verifier.VerifyIL("Program.M2", + isMissing ? """ +{ + // Code size 43 (0x2b) + .maxstack 4 + .locals init (int V_0, + System.Index V_1, + System.Span V_2) + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: call "System.Index Program.GetStart()" + IL_000b: stloc.1 + IL_000c: ldloca.s V_1 + IL_000e: ldc.i4.s 10 + IL_0010: call "int System.Index.GetOffset(int)" + IL_0015: stloc.0 + IL_0016: ldc.i4.s 10 + IL_0018: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_001d: stloc.2 + IL_001e: ldloca.s V_2 + IL_0020: ldloc.0 + IL_0021: ldc.i4.s 10 + IL_0023: ldloc.0 + IL_0024: sub + IL_0025: call "System.Span System.Span.Slice(int, int)" + IL_002a: ret +} +""" : """ +{ + // Code size 37 (0x25) + .maxstack 3 + .locals init (System.Span V_0, + System.Index V_1) + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: ldc.i4.s 10 + IL_0008: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: call "System.Index Program.GetStart()" + IL_0015: stloc.1 + IL_0016: ldloca.s V_1 + IL_0018: ldc.i4.s 10 + IL_001a: call "int System.Index.GetOffset(int)" + IL_001f: call "System.Span System.Span.Slice(int)" + IL_0024: ret +} +"""); + } + + [ConditionalFact(typeof(CoreClrOnly))] + public void SliceStart_04() + { + // slice with closed range `1..5` + var src = """ +class C +{ + public Buffer10 F; +} + +class Program +{ + static System.Span M(C x) => x.F[1..5]; +} +""" + Buffer10Definition; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseDll); + var verifier = CompileAndVerify(comp, verify: Verification.Fails).VerifyDiagnostics(); + + verifier.VerifyIL("Program.M", """ +{ + // Code size 24 (0x18) + .maxstack 3 + .locals init (System.Span V_0) + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: ldc.i4.s 10 + IL_0008: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: ldc.i4.1 + IL_0011: ldc.i4.4 + IL_0012: call "System.Span System.Span.Slice(int, int)" + IL_0017: ret +} +"""); + } + + [ConditionalFact(typeof(CoreClrOnly))] + public void SliceStart_05() + { + // slice with `0..` + var src = """ +class C +{ + public Buffer10 F; +} + +class Program +{ + static void Main() + { + var x = new C(); + var s = M(x); + System.Console.Write(s.Length); + } + + static System.Span M(C x) => x.F[0..]; +} +""" + Buffer10Definition; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "10", verify: Verification.Fails).VerifyDiagnostics(); + + // start=0 and rangeSize=10 are both constants within bounds, + // so InlineArrayAsSpan is created with the right length directly (no Slice). + verifier.VerifyIL("Program.M", """ +{ + // Code size 14 (0xe) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: ldc.i4.s 10 + IL_0008: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_000d: ret +} +"""); + } + + [ConditionalFact(typeof(CoreClrOnly))] + public void SliceStart_06() + { + // slice with Range parameter + var src = """ +class C +{ + public Buffer10 F; +} + +class Program +{ + static void Main() + { + Test1(); + System.Console.Write(' '); + Test2(); + } + + static void Test1() + { + var x = new C(); + M(x, 1..)[0] = 111; + System.Console.Write(M(x, 1..).Length); + System.Console.Write(' '); + System.Console.Write(x.F[1]); + } + + static void Test2() + { + var x = new C(); + M(x, 1..^1)[0] = 222; + System.Console.Write(M(x, 1..^1).Length); + System.Console.Write(' '); + System.Console.Write(x.F[1]); + } + + static System.Span M(C x, System.Range range) => x.F[range]; +} +""" + Buffer10Definition; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "9 111 8 222", verify: Verification.Fails).VerifyDiagnostics(); + + // Range parameter goes through DeconstructRange, always uses Slice(int, int) + verifier.VerifyIL("Program.M", """ +{ + // Code size 65 (0x41) + .maxstack 3 + .locals init (System.Range V_0, + int V_1, + int V_2, + System.Index V_3, + System.Span V_4) + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: ldarg.1 + IL_0007: stloc.0 + IL_0008: ldloca.s V_0 + IL_000a: call "System.Index System.Range.Start.get" + IL_000f: stloc.3 + IL_0010: ldloca.s V_3 + IL_0012: ldc.i4.s 10 + IL_0014: call "int System.Index.GetOffset(int)" + IL_0019: stloc.1 + IL_001a: ldloca.s V_0 + IL_001c: call "System.Index System.Range.End.get" + IL_0021: stloc.3 + IL_0022: ldloca.s V_3 + IL_0024: ldc.i4.s 10 + IL_0026: call "int System.Index.GetOffset(int)" + IL_002b: ldloc.1 + IL_002c: sub + IL_002d: stloc.2 + IL_002e: ldc.i4.s 10 + IL_0030: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_0035: stloc.s V_4 + IL_0037: ldloca.s V_4 + IL_0039: ldloc.1 + IL_003a: ldloc.2 + IL_003b: call "System.Span System.Span.Slice(int, int)" + IL_0040: ret +} +"""); + } + + [ConditionalFact(typeof(CoreClrOnly))] + public void SliceStart_07() + { + // slice with `..` + var src = """ +class C +{ + public Buffer10 F; +} + +class Program +{ + static void Main() + { + var x = new C(); + M(x)[0] = 111; + System.Console.Write($"{M(x).Length} {x.F[0]} {x.F[1]}"); + } + + static System.Span M(C x) => x.F[..]; +} +""" + Buffer10Definition; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + + var verifier = CompileAndVerify(comp, expectedOutput: "10 111 0", verify: Verification.Fails).VerifyDiagnostics(); + + verifier.VerifyIL("Program.M", """ +{ + // Code size 14 (0xe) + .maxstack 2 + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: ldc.i4.s 10 + IL_0008: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_000d: ret +} +"""); + } + + [ConditionalFact(typeof(CoreClrOnly))] + public void SliceStart_08() + { + // SubtractFromLength strategy in start.. + var src = """ +class C +{ + public Buffer10 F; +} + +class Program +{ + static void Main() + { + var x = new C(); + M(x)[0] = 111; + System.Console.Write($"{M(x).Length} {x.F[7]}"); + } + + static System.Span M(C x) => x.F[^GetStart()..]; + static int GetStart() { System.Console.Write("GetStart "); return 3; } +} +""" + Buffer10Definition; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "GetStart GetStart 3 111", verify: Verification.Fails).VerifyDiagnostics(); + + verifier.VerifyIL("Program.M", """ +{ + // Code size 30 (0x1e) + .maxstack 3 + .locals init (System.Span V_0) + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: ldc.i4.s 10 + IL_0008: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: ldc.i4.s 10 + IL_0012: call "int Program.GetStart()" + IL_0017: sub + IL_0018: call "System.Span System.Span.Slice(int)" + IL_001d: ret +} +"""); + } + + [ConditionalFact(typeof(CoreClrOnly))] + public void SliceStart_09() + { + // UseGetOffsetAPI strategy in start.. + var src = """ +class C +{ + public Buffer10 F; +} + +class Program +{ + static void Main() + { + var x = new C(); + M(x, 7)[0] = 111; + System.Console.Write($"{M(x, 7).Length} {x.F[7]}"); + } + + static System.Span M(C x, System.Index start) => x.F[start..]; +} +""" + Buffer10Definition; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net80, options: TestOptions.ReleaseExe); + var verifier = CompileAndVerify(comp, expectedOutput: "3 111", verify: Verification.Fails).VerifyDiagnostics(); + + verifier.VerifyIL("Program.M", """ +{ + // Code size 31 (0x1f) + .maxstack 3 + .locals init (System.Span V_0) + IL_0000: ldarg.0 + IL_0001: ldflda "Buffer10 C.F" + IL_0006: ldc.i4.s 10 + IL_0008: call "System.Span .InlineArrayAsSpan, int>(ref Buffer10, int)" + IL_000d: stloc.0 + IL_000e: ldloca.s V_0 + IL_0010: ldarga.s V_1 + IL_0012: ldc.i4.s 10 + IL_0014: call "int System.Index.GetOffset(int)" + IL_0019: call "System.Span System.Span.Slice(int)" + IL_001e: ret +} +"""); + } } } diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTestBase.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTestBase.cs index d81086d5ab74..91001e60578e 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTestBase.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTestBase.cs @@ -449,26 +449,6 @@ protected static void AssertEmpty(SymbolInfo info) Assert.Null(info.Symbol); Assert.Equal(CandidateReason.None, info.CandidateReason); } - - protected static void VerifyDecisionDagDump(Compilation comp, string expectedDecisionDag, int index = 0) - where T : CSharpSyntaxNode - { -#if DEBUG - var tree = comp.SyntaxTrees.First(); - var node = tree.GetRoot().DescendantNodes().OfType().ElementAt(index); - var model = (CSharpSemanticModel)comp.GetSemanticModel(tree); - var binder = model.GetEnclosingBinder(node.SpanStart); - var decisionDag = node switch - { - SwitchStatementSyntax n => ((BoundSwitchStatement)binder.BindStatement(n, BindingDiagnosticBag.Discarded)).ReachabilityDecisionDag, - SwitchExpressionSyntax n => ((BoundSwitchExpression)binder.BindExpression(n, BindingDiagnosticBag.Discarded)).ReachabilityDecisionDag, - IsPatternExpressionSyntax n => ((BoundIsPatternExpression)binder.BindExpression(n, BindingDiagnosticBag.Discarded)).ReachabilityDecisionDag, - var v => throw ExceptionUtilities.UnexpectedValue(v) - }; - - AssertEx.Equal(expectedDecisionDag, decisionDag.Dump()); -#endif - } #endregion helpers } } diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests4.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests4.cs index f64d4576ecce..693a6028244f 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests4.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests4.cs @@ -4164,7 +4164,7 @@ void M(C c) var binder = model.GetEnclosingBinder(@switch.SpanStart); var boundSwitch = (BoundSwitchStatement)binder.BindStatement(@switch, BindingDiagnosticBag.Discarded); AssertEx.Equal( -@"[0]: t0 is System.Runtime.CompilerServices.ITuple ? [1] : [28] +@"[0]: t0 != null ? [1] : [28] [1]: t1 = t0.Length; [2] [2]: t1 == 3 ? [3] : [28] [3]: t2 = t0[0]; [4] @@ -7186,13 +7186,16 @@ public void RedundantPattern_ITuplePattern_CompilerGenerated() _ = s is (_, _) and (_, _); // 1 _ = s is (_, _) and (_, var s5); _ = s is (_, _) and (_, _) s6; // 2, 3 +#line 13 _ = s is { Length: 2 } and (_, _); +#line 14 _ = s is not ({ Length: 2 } and (_, _)); _ = s switch { null => 0, { Length: not 2 } => 0, +#line 20 not (_, _) => 1, _ => 2, }; @@ -7212,6 +7215,15 @@ public void RedundantPattern_ITuplePattern_CompilerGenerated() // (12,21): error CS8129: No suitable 'Deconstruct' instance or extension method was found for type 'ITuple', with 2 out parameters and a void return type. // _ = s is (_, _) and (_, _) s6; // 2, 3 Diagnostic(ErrorCode.ERR_MissingDeconstruct, "(_, _)").WithArguments("System.Runtime.CompilerServices.ITuple", "2").WithLocation(12, 21), + // (13,28): hidden CS9335: The pattern is redundant. + // _ = s is { Length: 2 } and (_, _); + Diagnostic(ErrorCode.HDN_RedundantPattern, "(_, _)").WithLocation(13, 28), + // (14,33): hidden CS9335: The pattern is redundant. + // _ = s is not ({ Length: 2 } and (_, _)); + Diagnostic(ErrorCode.HDN_RedundantPattern, "(_, _)").WithLocation(14, 33), + // (20,5): error CS8510: The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match. + // not (_, _) => 1, + Diagnostic(ErrorCode.ERR_SwitchArmSubsumed, "not (_, _)").WithLocation(20, 5), // (24,5): error CS8518: An expression of type 'ITuple' can never match the provided pattern. // _ = s is (_, _, _) and (_, _); // 4 Diagnostic(ErrorCode.ERR_IsPatternImpossible, "s is (_, _, _) and (_, _)").WithArguments("System.Runtime.CompilerServices.ITuple").WithLocation(24, 5), diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests5.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests5.cs index 681f27e2d66e..dd530dc87211 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests5.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests5.cs @@ -2723,10 +2723,49 @@ private static int M(Enum e1, Enum e2, object o) """; var comp = CreateCompilation(source); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.e1; [1] +[1]: t1 == 1 ? [2] : [8] +[2]: t2 = t0.e2; [3] +[3]: t2 == 1 ? [4] : [5] +[4]: leaf `(Enum.One, Enum.One, _) => 0` +[5]: t2 == 2 ? [12] : [6] +[6]: t2 == 0 ? [7] : [18] +[7]: leaf `(Enum.One, Enum.Zero, _) => 0` +[8]: t1 == 2 ? [9] : [10] +[9]: leaf `(Enum.Two, _, _) => 0` +[10]: t2 = t0.e2; [11] +[11]: t2 == 2 ? [12] : [13] +[12]: leaf `(_, Enum.Two, _) => 0` +[13]: t1 == 0 ? [14] : [24] +[14]: t2 == 0 ? [15] : [16] +[15]: leaf `(Enum.Zero, Enum.Zero, _) => 0` +[16]: t2 == 1 ? [17] : [18] +[17]: leaf `(Enum.Zero, Enum.One, _) => 0` +[18]: t3 = t0.o; [19] +[19]: t3 is string ? [20] : [23] +[20]: t4 = (string)t3; [21] +[21]: when ? [22] : +[22]: leaf `(_, < 0 or > Enum.Two, string s) => 0` +[23]: leaf `(e1, e2, o) switch + { + (Enum.One, Enum.One, _) => 0, + (Enum.Two, _, _) => 0, + (_, Enum.Two, _) => 0, + (Enum.Zero, Enum.Zero, _) => 0, + (Enum.Zero, Enum.One, _) => 0, + (Enum.One, Enum.Zero, _) => 0, + ( < 0 or > Enum.Two, _, _) => 0, + (_, < 0 or > Enum.Two, string s) => 0, + }` +[24]: leaf `( < 0 or > Enum.Two, _, _) => 0` +"); + comp.VerifyDiagnostics( - // (12,28): warning CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Enum.One, (Enum)-1, _)' is not covered. + // (12,28): warning CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Enum.One, (Enum)3, _)' is not covered. // return (e1, e2, o) switch - Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveWithUnnamedEnumValue, "switch").WithArguments("(Enum.One, (Enum)-1, _)").WithLocation(12, 28) + Diagnostic(ErrorCode.WRN_SwitchExpressionNotExhaustiveWithUnnamedEnumValue, "switch").WithArguments("(Enum.One, (Enum)3, _)").WithLocation(12, 28) ); } @@ -3360,5 +3399,1713 @@ bool M0({type} x0) Diagnostic(ErrorCode.ERR_ConstantExpected, expression).WithLocation(6, 22) ); } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82063")] + public void SideeffectEvaluations_01() + { + var src = @" +interface I1 +{ + int F {get;} +} + +class C11; +class C12; +class C13(int f) : C12, I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C13.F ""); + return f; + } + } +} +class C14(int f) : I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C14.F ""); + return f; + } + } +} +class C15(int f) : I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C15.F ""); + return f; + } + } +} + +class Program +{ + static void Main() + { + object[] s = [null, new C11(), new C12(), new C13(1), new C14(1), new C15(1), new C13(2), new C14(2), new C15(2), new C13(3), new C14(3), new C15(3)]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((object, int) u) + { + return u is (C12 and I1 and { F: 1 }, 2) or (I1 and { F: 1 or 2 }, 1); + } +} +"; + var comp = CreateCompilation(src, options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: t1 is C12 ? [2] : [9] +[2]: t2 = (C12)t1; [3] +[3]: t2 is I1 ? [4] : [17] +[4]: t3 = (I1)t2; [5] +[5]: t4 = t3.F; [6] +[6]: t4 == 1 ? [7] : [13] +[7]: t5 = t0.Item2; [8] +[8]: t5 == 2 ? [16] : [15] +[9]: t1 is I1 ? [10] : [17] +[10]: t3 = (I1)t1; [11] +[11]: t4 = t3.F; [12] +[12]: t4 == 1 ? [14] : [13] +[13]: t4 == 2 ? [14] : [17] +[14]: t5 = t0.Item2; [15] +[15]: t5 == 1 ? [16] : [17] +[16]: leaf `(C12 and I1 and { F: 1 }, 2) or (I1 and { F: 1 or 2 }, 1)` +[17]: leaf `(C12 and I1 and { F: 1 }, 2) or (I1 and { F: 1 or 2 }, 1)` +"); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +False +False +False +False +False +False +False +False +False +Evaluated C13.F True +Evaluated C13.F True +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +Evaluated C15.F True +Evaluated C15.F False +Evaluated C15.F False +Evaluated C13.F True +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +Evaluated C15.F True +Evaluated C15.F False +Evaluated C15.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F False +Evaluated C14.F False +Evaluated C14.F False +Evaluated C15.F False +Evaluated C15.F False +Evaluated C15.F False +").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 102 (0x66) + .maxstack 2 + .locals init (object V_0, + C12 V_1, + I1 V_2, + int V_3, + int V_4, + bool V_5) + IL_0000: ldarg.0 + IL_0001: ldfld ""object System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: isinst ""C12"" + IL_000d: stloc.1 + IL_000e: ldloc.1 + IL_000f: brfalse.s IL_0035 + IL_0011: ldloc.1 + IL_0012: isinst ""I1"" + IL_0017: stloc.2 + IL_0018: ldloc.2 + IL_0019: brfalse.s IL_0060 + IL_001b: ldloc.2 + IL_001c: callvirt ""int I1.F.get"" + IL_0021: stloc.3 + IL_0022: ldloc.3 + IL_0023: ldc.i4.1 + IL_0024: bne.un.s IL_004a + IL_0026: ldarg.0 + IL_0027: ldfld ""int System.ValueTuple.Item2"" + IL_002c: stloc.s V_4 + IL_002e: ldloc.s V_4 + IL_0030: ldc.i4.2 + IL_0031: beq.s IL_005b + IL_0033: br.s IL_0056 + IL_0035: ldloc.0 + IL_0036: isinst ""I1"" + IL_003b: stloc.2 + IL_003c: ldloc.2 + IL_003d: brfalse.s IL_0060 + IL_003f: ldloc.2 + IL_0040: callvirt ""int I1.F.get"" + IL_0045: stloc.3 + IL_0046: ldloc.3 + IL_0047: ldc.i4.1 + IL_0048: beq.s IL_004e + IL_004a: ldloc.3 + IL_004b: ldc.i4.2 + IL_004c: bne.un.s IL_0060 + IL_004e: ldarg.0 + IL_004f: ldfld ""int System.ValueTuple.Item2"" + IL_0054: stloc.s V_4 + IL_0056: ldloc.s V_4 + IL_0058: ldc.i4.1 + IL_0059: bne.un.s IL_0060 + IL_005b: ldc.i4.1 + IL_005c: stloc.s V_5 + IL_005e: br.s IL_0063 + IL_0060: ldc.i4.0 + IL_0061: stloc.s V_5 + IL_0063: ldloc.s V_5 + IL_0065: ret +} +"); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82063")] + public void SideeffectEvaluations_02() + { + var src = @" +interface I1 +{ + int F {get;} +} + +class C11; +class C12; +class C13(int f) : C12, I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C13.F ""); + return f; + } + } +} +class C14(int f) : I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C14.F ""); + return f; + } + } +} +class C15(int f) : I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C15.F ""); + return f; + } + } +} + +class Program +{ + static void Main() + { + object[] s = [null, new C11(), new C12(), new C13(1), new C14(1), new C15(1), new C13(2), new C14(2), new C15(2), new C13(3), new C14(3), new C15(3)]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((object, int) u) + { + return u is (I1 and { F: 1 or 2 }, 1) or (C12 and I1 and { F: 1 }, 2); + } +} +"; + var comp = CreateCompilation(src, options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: t1 is I1 ? [2] : [13] +[2]: t2 = (I1)t1; [3] +[3]: t3 = t2.F; [4] +[4]: t3 == 1 ? [5] : [9] +[5]: t4 = t0.Item2; [6] +[6]: t4 == 1 ? [12] : [7] +[7]: t1 is C12 ? [8] : [13] +[8]: t4 == 2 ? [12] : [13] +[9]: t3 == 2 ? [10] : [13] +[10]: t4 = t0.Item2; [11] +[11]: t4 == 1 ? [12] : [13] +[12]: leaf `(I1 and { F: 1 or 2 }, 1) or (C12 and I1 and { F: 1 }, 2)` +[13]: leaf `(I1 and { F: 1 or 2 }, 1) or (C12 and I1 and { F: 1 }, 2)` +"); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +False +False +False +False +False +False +False +False +False +Evaluated C13.F True +Evaluated C13.F True +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +Evaluated C15.F True +Evaluated C15.F False +Evaluated C15.F False +Evaluated C13.F True +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +Evaluated C15.F True +Evaluated C15.F False +Evaluated C15.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F False +Evaluated C14.F False +Evaluated C14.F False +Evaluated C15.F False +Evaluated C15.F False +Evaluated C15.F False +").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 81 (0x51) + .maxstack 2 + .locals init (object V_0, + I1 V_1, + int V_2, + int V_3, + bool V_4) + IL_0000: ldarg.0 + IL_0001: ldfld ""object System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: isinst ""I1"" + IL_000d: stloc.1 + IL_000e: ldloc.1 + IL_000f: brfalse.s IL_004b + IL_0011: ldloc.1 + IL_0012: callvirt ""int I1.F.get"" + IL_0017: stloc.2 + IL_0018: ldloc.2 + IL_0019: ldc.i4.1 + IL_001a: beq.s IL_0022 + IL_001c: ldloc.2 + IL_001d: ldc.i4.2 + IL_001e: beq.s IL_003b + IL_0020: br.s IL_004b + IL_0022: ldarg.0 + IL_0023: ldfld ""int System.ValueTuple.Item2"" + IL_0028: stloc.3 + IL_0029: ldloc.3 + IL_002a: ldc.i4.1 + IL_002b: beq.s IL_0046 + IL_002d: ldloc.0 + IL_002e: isinst ""C12"" + IL_0033: brfalse.s IL_004b + IL_0035: ldloc.3 + IL_0036: ldc.i4.2 + IL_0037: beq.s IL_0046 + IL_0039: br.s IL_004b + IL_003b: ldarg.0 + IL_003c: ldfld ""int System.ValueTuple.Item2"" + IL_0041: stloc.3 + IL_0042: ldloc.3 + IL_0043: ldc.i4.1 + IL_0044: bne.un.s IL_004b + IL_0046: ldc.i4.1 + IL_0047: stloc.s V_4 + IL_0049: br.s IL_004e + IL_004b: ldc.i4.0 + IL_004c: stloc.s V_4 + IL_004e: ldloc.s V_4 + IL_0050: ret +} +"); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82063")] + public void SideeffectEvaluations_03() + { + var src = @" +interface I1 +{ + int F {get;} +} + +interface I2 : I1 +{ +} + +class C11; +class C12; +class C13(int f) : C12, I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C13.F ""); + return f; + } + } +} +class C14(int f) : I2 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C14.F ""); + return f; + } + } +} +class C15(int f) : I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C15.F ""); + return f; + } + } +} +class C16(int f) : C12, I2 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C15.F ""); + return f; + } + } +} + +class Program +{ + static void Main() + { + object[] s = [null, new C11(), new C12(), new C13(1), new C14(1), new C15(1), new C16(1), new C13(2), new C14(2), new C15(2), new C16(2), new C13(3), new C14(3), new C15(3), new C16(3)]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((object, int) u) + { + return u is (C12 and I1 and { F: 1 }, 2) or (I2 and { F: 1 or 2 }, 1); + } +} +"; + var comp = CreateCompilation(src, options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: t1 is C12 ? [2] : [12] +[2]: t2 = (C12)t1; [3] +[3]: t2 is I1 ? [4] : [20] +[4]: t3 = (I1)t2; [5] +[5]: t4 = t3.F; [6] +[6]: t4 == 1 ? [7] : [10] +[7]: t5 = t0.Item2; [8] +[8]: t5 == 2 ? [19] : [9] +[9]: t1 is I2 ? [18] : [20] +[10]: t1 is I2 ? [11] : [20] +[11]: t4 == 2 ? [17] : [20] +[12]: t1 is I2 ? [13] : [20] +[13]: t6 = (I2)t1; [14] +[14]: t7 = t6.F; [15] +[15]: t7 == 1 ? [17] : [16] +[16]: t7 == 2 ? [17] : [20] +[17]: t5 = t0.Item2; [18] +[18]: t5 == 1 ? [19] : [20] +[19]: leaf `(C12 and I1 and { F: 1 }, 2) or (I2 and { F: 1 or 2 }, 1)` +[20]: leaf `(C12 and I1 and { F: 1 }, 2) or (I2 and { F: 1 or 2 }, 1)` +"); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +False +False +False +False +False +False +False +False +False +Evaluated C13.F False +Evaluated C13.F True +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +False +False +False +Evaluated C15.F True +Evaluated C15.F True +Evaluated C15.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +False +False +False +Evaluated C15.F True +Evaluated C15.F False +Evaluated C15.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F False +Evaluated C14.F False +Evaluated C14.F False +False +False +False +Evaluated C15.F False +Evaluated C15.F False +Evaluated C15.F False +").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 127 (0x7f) + .maxstack 2 + .locals init (object V_0, + C12 V_1, + I1 V_2, + int V_3, + int V_4, + I2 V_5, + int V_6, + bool V_7) + IL_0000: ldarg.0 + IL_0001: ldfld ""object System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: isinst ""C12"" + IL_000d: stloc.1 + IL_000e: ldloc.1 + IL_000f: brfalse.s IL_004b + IL_0011: ldloc.1 + IL_0012: isinst ""I1"" + IL_0017: stloc.2 + IL_0018: ldloc.2 + IL_0019: brfalse.s IL_0079 + IL_001b: ldloc.2 + IL_001c: callvirt ""int I1.F.get"" + IL_0021: stloc.3 + IL_0022: ldloc.3 + IL_0023: ldc.i4.1 + IL_0024: bne.un.s IL_003d + IL_0026: ldarg.0 + IL_0027: ldfld ""int System.ValueTuple.Item2"" + IL_002c: stloc.s V_4 + IL_002e: ldloc.s V_4 + IL_0030: ldc.i4.2 + IL_0031: beq.s IL_0074 + IL_0033: ldloc.0 + IL_0034: isinst ""I2"" + IL_0039: brtrue.s IL_006f + IL_003b: br.s IL_0079 + IL_003d: ldloc.0 + IL_003e: isinst ""I2"" + IL_0043: brfalse.s IL_0079 + IL_0045: ldloc.3 + IL_0046: ldc.i4.2 + IL_0047: beq.s IL_0067 + IL_0049: br.s IL_0079 + IL_004b: ldloc.0 + IL_004c: isinst ""I2"" + IL_0051: stloc.s V_5 + IL_0053: ldloc.s V_5 + IL_0055: brfalse.s IL_0079 + IL_0057: ldloc.s V_5 + IL_0059: callvirt ""int I1.F.get"" + IL_005e: stloc.s V_6 + IL_0060: ldloc.s V_6 + IL_0062: ldc.i4.1 + IL_0063: sub + IL_0064: ldc.i4.1 + IL_0065: bgt.un.s IL_0079 + IL_0067: ldarg.0 + IL_0068: ldfld ""int System.ValueTuple.Item2"" + IL_006d: stloc.s V_4 + IL_006f: ldloc.s V_4 + IL_0071: ldc.i4.1 + IL_0072: bne.un.s IL_0079 + IL_0074: ldc.i4.1 + IL_0075: stloc.s V_7 + IL_0077: br.s IL_007c + IL_0079: ldc.i4.0 + IL_007a: stloc.s V_7 + IL_007c: ldloc.s V_7 + IL_007e: ret +} +"); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82063")] + public void SideeffectEvaluations_04() + { + var src = @" +interface I1 +{ + int F {get;} +} + +interface I2 : I1 +{ +} + +class C11; +class C12; +class C13(int f) : C12, I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C13.F ""); + return f; + } + } +} +class C14(int f) : I2 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C14.F ""); + return f; + } + } +} +class C15(int f) : I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C15.F ""); + return f; + } + } +} +class C16(int f) : C12, I2 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C15.F ""); + return f; + } + } +} + +class Program +{ + static void Main() + { + object[] s = [null, new C11(), new C12(), new C13(1), new C14(1), new C15(1), new C16(1), new C13(2), new C14(2), new C15(2), new C16(2), new C13(3), new C14(3), new C15(3), new C16(3)]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((object, int) u) + { + return u is (C12 and I2 and { F: 1 }, 2) or (I1 and { F: 1 or 2 }, 1); + } +} +"; + var comp = CreateCompilation(src, options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: t1 is C12 ? [2] : [10] +[2]: t2 = (C12)t1; [3] +[3]: t2 is I2 ? [4] : [10] +[4]: t3 = (I2)t2; [5] +[5]: t4 = t3.F; [6] +[6]: t4 == 1 ? [7] : [9] +[7]: t5 = t0.Item2; [8] +[8]: t5 == 2 ? [17] : [16] +[9]: t4 == 2 ? [15] : [18] +[10]: t1 is I1 ? [11] : [18] +[11]: t6 = (I1)t1; [12] +[12]: t7 = t6.F; [13] +[13]: t7 == 1 ? [15] : [14] +[14]: t7 == 2 ? [15] : [18] +[15]: t5 = t0.Item2; [16] +[16]: t5 == 1 ? [17] : [18] +[17]: leaf `(C12 and I2 and { F: 1 }, 2) or (I1 and { F: 1 or 2 }, 1)` +[18]: leaf `(C12 and I2 and { F: 1 }, 2) or (I1 and { F: 1 or 2 }, 1)` +"); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +False +False +False +False +False +False +False +False +False +Evaluated C13.F True +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +Evaluated C15.F True +Evaluated C15.F False +Evaluated C15.F False +Evaluated C15.F True +Evaluated C15.F True +Evaluated C15.F False +Evaluated C13.F True +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +Evaluated C15.F True +Evaluated C15.F False +Evaluated C15.F False +Evaluated C15.F True +Evaluated C15.F False +Evaluated C15.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F False +Evaluated C14.F False +Evaluated C14.F False +Evaluated C15.F False +Evaluated C15.F False +Evaluated C15.F False +Evaluated C15.F False +Evaluated C15.F False +Evaluated C15.F False +").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 111 (0x6f) + .maxstack 2 + .locals init (object V_0, + C12 V_1, + I2 V_2, + int V_3, + int V_4, + I1 V_5, + int V_6, + bool V_7) + IL_0000: ldarg.0 + IL_0001: ldfld ""object System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: isinst ""C12"" + IL_000d: stloc.1 + IL_000e: ldloc.1 + IL_000f: brfalse.s IL_003b + IL_0011: ldloc.1 + IL_0012: isinst ""I2"" + IL_0017: stloc.2 + IL_0018: ldloc.2 + IL_0019: brfalse.s IL_003b + IL_001b: ldloc.2 + IL_001c: callvirt ""int I1.F.get"" + IL_0021: stloc.3 + IL_0022: ldloc.3 + IL_0023: ldc.i4.1 + IL_0024: beq.s IL_002c + IL_0026: ldloc.3 + IL_0027: ldc.i4.2 + IL_0028: beq.s IL_0057 + IL_002a: br.s IL_0069 + IL_002c: ldarg.0 + IL_002d: ldfld ""int System.ValueTuple.Item2"" + IL_0032: stloc.s V_4 + IL_0034: ldloc.s V_4 + IL_0036: ldc.i4.2 + IL_0037: beq.s IL_0064 + IL_0039: br.s IL_005f + IL_003b: ldloc.0 + IL_003c: isinst ""I1"" + IL_0041: stloc.s V_5 + IL_0043: ldloc.s V_5 + IL_0045: brfalse.s IL_0069 + IL_0047: ldloc.s V_5 + IL_0049: callvirt ""int I1.F.get"" + IL_004e: stloc.s V_6 + IL_0050: ldloc.s V_6 + IL_0052: ldc.i4.1 + IL_0053: sub + IL_0054: ldc.i4.1 + IL_0055: bgt.un.s IL_0069 + IL_0057: ldarg.0 + IL_0058: ldfld ""int System.ValueTuple.Item2"" + IL_005d: stloc.s V_4 + IL_005f: ldloc.s V_4 + IL_0061: ldc.i4.1 + IL_0062: bne.un.s IL_0069 + IL_0064: ldc.i4.1 + IL_0065: stloc.s V_7 + IL_0067: br.s IL_006c + IL_0069: ldc.i4.0 + IL_006a: stloc.s V_7 + IL_006c: ldloc.s V_7 + IL_006e: ret +} +"); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82063")] + public void SideeffectEvaluations_05() + { + var src = @" +interface I1 +{ + int F {get;} +} + +interface I2 : I1 +{ +} + +class C11; +class C12; +class C13(int f) : C12, I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C13.F ""); + return f; + } + } +} +class C14(int f) : I2 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C14.F ""); + return f; + } + } +} +class C15(int f) : I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C15.F ""); + return f; + } + } +} +class C16(int f) : C12, I2 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C15.F ""); + return f; + } + } +} + +class Program +{ + static void Main() + { + object[] s = [null, new C11(), new C12(), new C13(1), new C14(1), new C15(1), new C16(1), new C13(2), new C14(2), new C15(2), new C16(2), new C13(3), new C14(3), new C15(3), new C16(3)]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(t); + } + } + } + + static int Test1((object, int) u) + { + return u switch { (C12 and I1 and { F: 1 and var x1 }, 2) => x1 ,(I2 and { F: (1 or 2) and var x2 }, 1) => x2, _ => -100 }; + } +} +"; + var comp = CreateCompilation(src, options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: t1 is C12 ? [2] : [17] +[2]: t2 = (C12)t1; [3] +[3]: t2 is I1 ? [4] : [26] +[4]: t3 = (I1)t2; [5] +[5]: t4 = t3.F; [6] +[6]: t4 == 1 ? [7] : [12] +[7]: t5 = t0.Item2; [8] +[8]: t5 == 2 ? [9] : [11] +[9]: when ? [10] : +[10]: leaf `(C12 and I1 and { F: 1 and var x1 }, 2) => x1` +[11]: t1 is I2 ? [15] : [26] +[12]: t1 is I2 ? [13] : [26] +[13]: t4 == 2 ? [14] : [26] +[14]: t5 = t0.Item2; [15] +[15]: t5 == 1 ? [16] : [26] +[16]: t8 <-- t4; [24] +[17]: t1 is I2 ? [18] : [26] +[18]: t7 = (I2)t1; [19] +[19]: t8 = t7.F; [20] +[20]: t8 == 1 ? [22] : [21] +[21]: t8 == 2 ? [22] : [26] +[22]: t5 = t0.Item2; [23] +[23]: t5 == 1 ? [24] : [26] +[24]: when ? [25] : +[25]: leaf `(I2 and { F: (1 or 2) and var x2 }, 1) => x2` +[26]: leaf `_ => -100` +"); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +-100 +-100 +-100 +-100 +-100 +-100 +-100 +-100 +-100 +Evaluated C13.F -100 +Evaluated C13.F 1 +Evaluated C13.F -100 +Evaluated C14.F 1 +Evaluated C14.F -100 +Evaluated C14.F -100 +-100 +-100 +-100 +Evaluated C15.F 1 +Evaluated C15.F 1 +Evaluated C15.F -100 +Evaluated C13.F -100 +Evaluated C13.F -100 +Evaluated C13.F -100 +Evaluated C14.F 2 +Evaluated C14.F -100 +Evaluated C14.F -100 +-100 +-100 +-100 +Evaluated C15.F 2 +Evaluated C15.F -100 +Evaluated C15.F -100 +Evaluated C13.F -100 +Evaluated C13.F -100 +Evaluated C13.F -100 +Evaluated C14.F -100 +Evaluated C14.F -100 +Evaluated C14.F -100 +-100 +-100 +-100 +Evaluated C15.F -100 +Evaluated C15.F -100 +Evaluated C15.F -100 +").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 150 (0x96) + .maxstack 2 + .locals init (int V_0, //x1 + int V_1, //x2 + int V_2, + object V_3, + C12 V_4, + I1 V_5, + int V_6, + I2 V_7) + IL_0000: ldarg.0 + IL_0001: ldfld ""object System.ValueTuple.Item1"" + IL_0006: stloc.3 + IL_0007: ldloc.3 + IL_0008: isinst ""C12"" + IL_000d: stloc.s V_4 + IL_000f: ldloc.s V_4 + IL_0011: brfalse.s IL_0060 + IL_0013: ldloc.s V_4 + IL_0015: isinst ""I1"" + IL_001a: stloc.s V_5 + IL_001c: ldloc.s V_5 + IL_001e: brfalse.s IL_0091 + IL_0020: ldloc.s V_5 + IL_0022: callvirt ""int I1.F.get"" + IL_0027: stloc.0 + IL_0028: ldloc.0 + IL_0029: ldc.i4.1 + IL_002a: bne.un.s IL_0043 + IL_002c: ldarg.0 + IL_002d: ldfld ""int System.ValueTuple.Item2"" + IL_0032: stloc.s V_6 + IL_0034: ldloc.s V_6 + IL_0036: ldc.i4.2 + IL_0037: beq.s IL_0089 + IL_0039: ldloc.3 + IL_003a: isinst ""I2"" + IL_003f: brtrue.s IL_0057 + IL_0041: br.s IL_0091 + IL_0043: ldloc.3 + IL_0044: isinst ""I2"" + IL_0049: brfalse.s IL_0091 + IL_004b: ldloc.0 + IL_004c: ldc.i4.2 + IL_004d: bne.un.s IL_0091 + IL_004f: ldarg.0 + IL_0050: ldfld ""int System.ValueTuple.Item2"" + IL_0055: stloc.s V_6 + IL_0057: ldloc.s V_6 + IL_0059: ldc.i4.1 + IL_005a: bne.un.s IL_0091 + IL_005c: ldloc.0 + IL_005d: stloc.1 + IL_005e: br.s IL_008d + IL_0060: ldloc.3 + IL_0061: isinst ""I2"" + IL_0066: stloc.s V_7 + IL_0068: ldloc.s V_7 + IL_006a: brfalse.s IL_0091 + IL_006c: ldloc.s V_7 + IL_006e: callvirt ""int I1.F.get"" + IL_0073: stloc.1 + IL_0074: ldloc.1 + IL_0075: ldc.i4.1 + IL_0076: sub + IL_0077: ldc.i4.1 + IL_0078: bgt.un.s IL_0091 + IL_007a: ldarg.0 + IL_007b: ldfld ""int System.ValueTuple.Item2"" + IL_0080: stloc.s V_6 + IL_0082: ldloc.s V_6 + IL_0084: ldc.i4.1 + IL_0085: beq.s IL_008d + IL_0087: br.s IL_0091 + IL_0089: ldloc.0 + IL_008a: stloc.2 + IL_008b: br.s IL_0094 + IL_008d: ldloc.1 + IL_008e: stloc.2 + IL_008f: br.s IL_0094 + IL_0091: ldc.i4.s -100 + IL_0093: stloc.2 + IL_0094: ldloc.2 + IL_0095: ret +} +"); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82063")] + public void SideeffectEvaluations_06() + { + var src = @" +#nullable enable + +interface I1 +{ + C2? F { get; } +} + +class C2 +{ + public string? S = null; +} + +interface I2 : I1 +{ +} + +class C11; +class C12; +class C13 : C12, I1 +{ + public C2? F => null; +} +class C14 : I2 +{ + public C2? F => null; +} +class C15 : I1 +{ + public C2? F => null; +} +class C16 : C12, I2 +{ + public C2? F => null; +} + +class Program +{ + static int Test1((object, int) u) + { + return u switch + { + (C12 and I1 and { F: { S: ""1"" and var x1 } }, 2) => x1.Length , + (I2 and { F: { S: (""1"" or ""2"") and var x2 } }, 1) => x2.Length, + _ => -100 + }; + } + + static int Test2((object, int) u) + { + return u switch + { + (C12 and I1 and { F: { S: ""1"" } x3 }, 2) => x3.S.Length , + (I2 and { F: { S: (""1"" or ""2"") } x4 }, 1) => x4.S.Length, + _ => -100 + }; + } +} +"; + var comp = CreateCompilation(src, options: TestOptions.ReleaseDll); + comp.VerifyDiagnostics(); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82063")] + public void SideeffectEvaluations_07() + { + var src = @" +class C0; + +class C01(bool p) : C0 +{ + public bool P => p; +} + + +class C1(bool p) : C01(p); + +class C02(bool p) : C0 +{ + public bool P => p; +} + +class C2(bool p) : C02(p); + +class C3(bool p) : C01(p); + +class C4(bool p) : C01(p); + +class C03(bool p) : C0 +{ + public bool P => p; +} + +class C5(bool p) : C03(p); + +class C6(bool p) : C01(p); + +static class Program +{ + static void Main() + { + C0[] s = [null, new C1(true), new C2(true), new C3(true), new C4(true), new C5(true), new C6(true), new C1(false), new C2(false), new C3(false), new C4(false), new C5(false), new C6(false)]; + foreach (var s1 in s) + { + var t = s1.Test1(); + System.Console.WriteLine(t); + } + } + + internal static bool Test1(this C0 c) + { + return c + is C1 { P: true } + or C2 { P: true } + or C3 { P: true } + or C4 { P: true } + or C5 { P: true } + or C6 { P: true }; + } +} +"; + var comp = CreateCompilation(src, options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t0 is C1 ? [1] : [4] +[1]: t1 = (C1)t0; [2] +[2]: t2 = t1.P; [3] +[3]: t2 == True ? [24] : [25] +[4]: t0 is C2 ? [5] : [8] +[5]: t3 = (C2)t0; [6] +[6]: t4 = t3.P; [7] +[7]: t4 == True ? [24] : [25] +[8]: t0 is C3 ? [9] : [12] +[9]: t5 = (C3)t0; [10] +[10]: t6 = t5.P; [11] +[11]: t6 == True ? [24] : [25] +[12]: t0 is C4 ? [13] : [16] +[13]: t7 = (C4)t0; [14] +[14]: t8 = t7.P; [15] +[15]: t8 == True ? [24] : [25] +[16]: t0 is C5 ? [17] : [20] +[17]: t9 = (C5)t0; [18] +[18]: t10 = t9.P; [19] +[19]: t10 == True ? [24] : [25] +[20]: t0 is C6 ? [21] : [25] +[21]: t11 = (C6)t0; [22] +[22]: t12 = t11.P; [23] +[23]: t12 == True ? [24] : [25] +[24]: leaf `C1 { P: true } + or C2 { P: true } + or C3 { P: true } + or C4 { P: true } + or C5 { P: true } + or C6 { P: true }` +[25]: leaf `C1 { P: true } + or C2 { P: true } + or C3 { P: true } + or C4 { P: true } + or C5 { P: true } + or C6 { P: true }` +"); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +False +True +True +True +True +True +True +False +False +False +False +False +False +").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 135 (0x87) + .maxstack 1 + .locals init (C1 V_0, + C2 V_1, + C3 V_2, + C4 V_3, + C5 V_4, + C6 V_5, + bool V_6) + IL_0000: ldarg.0 + IL_0001: isinst ""C1"" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: brfalse.s IL_0014 + IL_000a: ldloc.0 + IL_000b: callvirt ""bool C01.P.get"" + IL_0010: brtrue.s IL_007c + IL_0012: br.s IL_0081 + IL_0014: ldarg.0 + IL_0015: isinst ""C2"" + IL_001a: stloc.1 + IL_001b: ldloc.1 + IL_001c: brfalse.s IL_0028 + IL_001e: ldloc.1 + IL_001f: callvirt ""bool C02.P.get"" + IL_0024: brtrue.s IL_007c + IL_0026: br.s IL_0081 + IL_0028: ldarg.0 + IL_0029: isinst ""C3"" + IL_002e: stloc.2 + IL_002f: ldloc.2 + IL_0030: brfalse.s IL_003c + IL_0032: ldloc.2 + IL_0033: callvirt ""bool C01.P.get"" + IL_0038: brtrue.s IL_007c + IL_003a: br.s IL_0081 + IL_003c: ldarg.0 + IL_003d: isinst ""C4"" + IL_0042: stloc.3 + IL_0043: ldloc.3 + IL_0044: brfalse.s IL_0050 + IL_0046: ldloc.3 + IL_0047: callvirt ""bool C01.P.get"" + IL_004c: brtrue.s IL_007c + IL_004e: br.s IL_0081 + IL_0050: ldarg.0 + IL_0051: isinst ""C5"" + IL_0056: stloc.s V_4 + IL_0058: ldloc.s V_4 + IL_005a: brfalse.s IL_0067 + IL_005c: ldloc.s V_4 + IL_005e: callvirt ""bool C03.P.get"" + IL_0063: brtrue.s IL_007c + IL_0065: br.s IL_0081 + IL_0067: ldarg.0 + IL_0068: isinst ""C6"" + IL_006d: stloc.s V_5 + IL_006f: ldloc.s V_5 + IL_0071: brfalse.s IL_0081 + IL_0073: ldloc.s V_5 + IL_0075: callvirt ""bool C01.P.get"" + IL_007a: brfalse.s IL_0081 + IL_007c: ldc.i4.1 + IL_007d: stloc.s V_6 + IL_007f: br.s IL_0084 + IL_0081: ldc.i4.0 + IL_0082: stloc.s V_6 + IL_0084: ldloc.s V_6 + IL_0086: ret +} +"); + } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82063")] + public void SideeffectEvaluations_08() + { + var src = @" +interface I1 +{ + int F {get;} +} + +class C12; +class C13(int f) : C12, I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C13.F ""); + return f; + } + } +} +class C14(int f) : I1 +{ + public int F + { + get + { + System.Console.Write(""Evaluated C14.F ""); + return f; + } + } +} + +class Program +{ + static void Main() + { + I1[] s = [null, new C13(1), new C14(1), new C13(2), new C14(2), new C13(3), new C14(3)]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((I1, int) u) + { + return u is (C12 and I1 and { F: 1 }, 2) or ({ F: 1 or 2 }, 1); + } +} +"; + var comp = CreateCompilation(src, options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: t1 is C12 ? [2] : [8] +[2]: t2 = (C12)t1; [3] +[3]: t2 is I1 ? [4] : [9] +[4]: t3 = t1.F; [5] +[5]: t3 == 1 ? [6] : [11] +[6]: t4 = t0.Item2; [7] +[7]: t4 == 2 ? [14] : [13] +[8]: t1 != null ? [9] : [15] +[9]: t3 = t1.F; [10] +[10]: t3 == 1 ? [12] : [11] +[11]: t3 == 2 ? [12] : [15] +[12]: t4 = t0.Item2; [13] +[13]: t4 == 1 ? [14] : [15] +[14]: leaf `(C12 and I1 and { F: 1 }, 2) or ({ F: 1 or 2 }, 1)` +[15]: leaf `(C12 and I1 and { F: 1 }, 2) or ({ F: 1 or 2 }, 1)` +"); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +False +False +False +Evaluated C13.F True +Evaluated C13.F True +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +Evaluated C13.F True +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F True +Evaluated C14.F False +Evaluated C14.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C13.F False +Evaluated C14.F False +Evaluated C14.F False +Evaluated C14.F False +").VerifyDiagnostics( + // (49,30): hidden CS9335: The pattern is redundant. + // return u is (C12 and I1 and { F: 1 }, 2) or ({ F: 1 or 2 }, 1); + Diagnostic(ErrorCode.HDN_RedundantPattern, "I1").WithLocation(49, 30) + ); + + verifier.VerifyIL("Program.Test1", @" +{ + // Code size 89 (0x59) + .maxstack 2 + .locals init (I1 V_0, + C12 V_1, + int V_2, + int V_3, + bool V_4) + IL_0000: ldarg.0 + IL_0001: ldfld ""I1 System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: isinst ""C12"" + IL_000d: stloc.1 + IL_000e: ldloc.1 + IL_000f: brfalse.s IL_0031 + IL_0011: ldloc.1 + IL_0012: isinst ""I1"" + IL_0017: brfalse.s IL_0034 + IL_0019: ldloc.0 + IL_001a: callvirt ""int I1.F.get"" + IL_001f: stloc.2 + IL_0020: ldloc.2 + IL_0021: ldc.i4.1 + IL_0022: bne.un.s IL_003f + IL_0024: ldarg.0 + IL_0025: ldfld ""int System.ValueTuple.Item2"" + IL_002a: stloc.3 + IL_002b: ldloc.3 + IL_002c: ldc.i4.2 + IL_002d: beq.s IL_004e + IL_002f: br.s IL_004a + IL_0031: ldloc.0 + IL_0032: brfalse.s IL_0053 + IL_0034: ldloc.0 + IL_0035: callvirt ""int I1.F.get"" + IL_003a: stloc.2 + IL_003b: ldloc.2 + IL_003c: ldc.i4.1 + IL_003d: beq.s IL_0043 + IL_003f: ldloc.2 + IL_0040: ldc.i4.2 + IL_0041: bne.un.s IL_0053 + IL_0043: ldarg.0 + IL_0044: ldfld ""int System.ValueTuple.Item2"" + IL_0049: stloc.3 + IL_004a: ldloc.3 + IL_004b: ldc.i4.1 + IL_004c: bne.un.s IL_0053 + IL_004e: ldc.i4.1 + IL_004f: stloc.s V_4 + IL_0051: br.s IL_0056 + IL_0053: ldc.i4.0 + IL_0054: stloc.s V_4 + IL_0056: ldloc.s V_4 + IL_0058: ret +} +"); + } + + [Fact] + public void SideeffectEvaluations_09() + { + var src = @" +using System; + +class C11; + +class C12; + +struct S1 +{ + + static void Main() + { + object[] s = [null, (new C11()), (new C12()), (1), (""1""), (2), (""2""), (3), (""3"")]; + int[] i = [1, 2, 3]; + foreach (var s1 in s) + { + foreach (var j in i) + { + var t = Test1((s1, j)); + System.Console.WriteLine(t); + } + } + } + + static bool Test1((object, int) u) + { + return u is (int and 1, 2) or (System.IComparable and { AsInt: 3 }, 1); + } +} + +static class IComparableExtensions +{ + extension(IComparable c) + { + public int? AsInt => c as int?; + } +} +"; + var comp = CreateCompilation(src, options: TestOptions.ReleaseExe); + + VerifyDecisionDagDump(comp, +@"[0]: t1 = t0.Item1; [1] +[1]: t1 is int ? [2] : [11] +[2]: t2 = (int)t1; [3] +[3]: t2 == 1 ? [4] : [12] +[4]: t3 = t0.Item2; [5] +[5]: t3 == 2 ? [19] : [6] +[6]: t4 = (System.IComparable)t1; [7] +[7]: t5 = t4.AsInt; [8] +[8]: t5 != null ? [9] : [20] +[9]: t6 = (int)t5; [10] +[10]: t6 == 3 ? [18] : [20] +[11]: t1 is System.IComparable ? [12] : [20] +[12]: t4 = (System.IComparable)t1; [13] +[13]: t5 = t4.AsInt; [14] +[14]: t5 != null ? [15] : [20] +[15]: t6 = (int)t5; [16] +[16]: t6 == 3 ? [17] : [20] +[17]: t3 = t0.Item2; [18] +[18]: t3 == 1 ? [19] : [20] +[19]: leaf `(int and 1, 2) or (System.IComparable and { AsInt: 3 }, 1)` +[20]: leaf `(int and 1, 2) or (System.IComparable and { AsInt: 3 }, 1)` +", +forLowering: true); + + var verifier = CompileAndVerify( + comp, + expectedOutput: @" +False +False +False +False +False +False +False +False +False +False +True +False +False +False +False +False +False +False +False +False +False +True +False +False +False +False +False +").VerifyDiagnostics(); + + verifier.VerifyIL("S1.Test1", @" +{ + // Code size 137 (0x89) + .maxstack 2 + .locals init (object V_0, + int V_1, + System.IComparable V_2, + int? V_3, + bool V_4) + IL_0000: ldarg.0 + IL_0001: ldfld ""object System.ValueTuple.Item1"" + IL_0006: stloc.0 + IL_0007: ldloc.0 + IL_0008: isinst ""int"" + IL_000d: brfalse.s IL_0046 + IL_000f: ldloc.0 + IL_0010: unbox.any ""int"" + IL_0015: ldc.i4.1 + IL_0016: bne.un.s IL_0052 + IL_0018: ldarg.0 + IL_0019: ldfld ""int System.ValueTuple.Item2"" + IL_001e: stloc.1 + IL_001f: ldloc.1 + IL_0020: ldc.i4.2 + IL_0021: beq.s IL_007e + IL_0023: ldloc.0 + IL_0024: castclass ""System.IComparable"" + IL_0029: stloc.2 + IL_002a: ldloc.2 + IL_002b: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_0030: stloc.3 + IL_0031: ldloca.s V_3 + IL_0033: call ""bool int?.HasValue.get"" + IL_0038: brfalse.s IL_0083 + IL_003a: ldloca.s V_3 + IL_003c: call ""int int?.GetValueOrDefault()"" + IL_0041: ldc.i4.3 + IL_0042: beq.s IL_007a + IL_0044: br.s IL_0083 + IL_0046: ldloc.0 + IL_0047: isinst ""System.IComparable"" + IL_004c: stloc.2 + IL_004d: ldloc.2 + IL_004e: brtrue.s IL_0059 + IL_0050: br.s IL_0083 + IL_0052: ldloc.0 + IL_0053: castclass ""System.IComparable"" + IL_0058: stloc.2 + IL_0059: ldloc.2 + IL_005a: call ""int? IComparableExtensions.get_AsInt(System.IComparable)"" + IL_005f: stloc.3 + IL_0060: ldloca.s V_3 + IL_0062: call ""bool int?.HasValue.get"" + IL_0067: brfalse.s IL_0083 + IL_0069: ldloca.s V_3 + IL_006b: call ""int int?.GetValueOrDefault()"" + IL_0070: ldc.i4.3 + IL_0071: bne.un.s IL_0083 + IL_0073: ldarg.0 + IL_0074: ldfld ""int System.ValueTuple.Item2"" + IL_0079: stloc.1 + IL_007a: ldloc.1 + IL_007b: ldc.i4.1 + IL_007c: bne.un.s IL_0083 + IL_007e: ldc.i4.1 + IL_007f: stloc.s V_4 + IL_0081: br.s IL_0086 + IL_0083: ldc.i4.0 + IL_0084: stloc.s V_4 + IL_0086: ldloc.s V_4 + IL_0088: ret +} +"); + } } } diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests_ListPatterns.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests_ListPatterns.cs index 0b958d607701..d7085ebb0d14 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests_ListPatterns.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/PatternMatchingTests_ListPatterns.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -1123,7 +1123,7 @@ public void M(string s) } "; var compilation = CreateCompilationWithIndexAndRange(source); - compilation.MakeMemberMissing(SpecialMember.System_String__Substring); + compilation.MakeMemberMissing(SpecialMember.System_String__SubstringIntInt); compilation.VerifyEmitDiagnostics( // (6,19): error CS0656: Missing compiler required member 'System.String.Substring' // _ = s is [.. var slice]; @@ -2444,11 +2444,8 @@ public static void Main() True True True -Length True -Length True -Length True "; var verifier = CompileAndVerify(compilation, expectedOutput: expectedOutput); @@ -2464,17 +2461,12 @@ .maxstack 2 }"), () => verifier.VerifyIL("X.Test2", @" { - // Code size 14 (0xe) - .maxstack 1 + // Code size 5 (0x5) + .maxstack 2 IL_0000: ldarg.0 - IL_0001: brfalse.s IL_000c - IL_0003: ldarg.0 - IL_0004: callvirt ""int C.Length.get"" - IL_0009: pop - IL_000a: ldc.i4.1 - IL_000b: ret - IL_000c: ldc.i4.0 - IL_000d: ret + IL_0001: ldnull + IL_0002: cgt.un + IL_0004: ret }") ); } @@ -2517,7 +2509,7 @@ public static void Main() [InlineData( "{ null, null, new(0, 0) }", "[..{ Length: >=2 }, { X: 0, Y: 0 }]", - "e.Length, e[0..^1], e[0..^1].Length, e[^1], e[^1].X, e[^1].Y, True")] + "e.Length, e[^1], e[^1].X, e[^1].Y, True")] [InlineData( "{ null, null, new(0, 0) }", "[.., { X: 0, Y: 0 }]", @@ -3508,8 +3500,9 @@ public class C "_ = new C()[..];" }; - foreach (var source in sources) + for (int i = 0; i < sources.Length; i++) { + string source = sources[i]; var comp = CreateCompilation(source, references: new[] { libComp.EmitToImageReference(), rangeRef }); comp.VerifyDiagnostics(); var used = comp.GetUsedAssemblyReferences(); @@ -4006,7 +3999,7 @@ public void M() } [Theory] - [InlineData("[.._]", "Length True")] + [InlineData("[.._]", "True")] [InlineData("[..]", "True")] [InlineData("[..var unused]", "Length Slice True")] [InlineData("[42, ..]", "Length Index True")] @@ -6342,16 +6335,13 @@ void Test(int[] a) ); VerifyDecisionDagDump(comp, -@"[0]: t0 != null ? [1] : [9] +@"[0]: t0 != null ? [1] : [6] [1]: t1 = t0.Length; [2] -[2]: t1 >= 1 ? [3] : [9] +[2]: t1 >= 1 ? [3] : [6] [3]: t2 = t0[-1]; [4] [4]: t2 == 42 ? [5] : [6] [5]: leaf `case [..,42]:` -[6]: t1 == 1 ? [7] : [9] -[7]: t3 = t0[0]; [8] -[8]: t3 <-- t2; [9] -[9]: leaf `switch (a) +[6]: leaf `switch (a) { case [..,42]: case [42]: @@ -6384,28 +6374,19 @@ void Test(int[] a, int[] b) VerifyDecisionDagDump(comp, @"[0]: t1 = t0.a; [1] -[1]: t1 != null ? [2] : [22] +[1]: t1 != null ? [2] : [13] [2]: t2 = t1.Length; [3] -[3]: t2 >= 1 ? [4] : [22] +[3]: t2 >= 1 ? [4] : [13] [4]: t3 = t1[-1]; [5] -[5]: t3 == 42 ? [6] : [19] +[5]: t3 == 42 ? [6] : [13] [6]: t4 = t0.b; [7] -[7]: t4 != null ? [8] : [22] +[7]: t4 != null ? [8] : [13] [8]: t5 = t4.Length; [9] -[9]: t5 >= 1 ? [10] : [22] +[9]: t5 >= 1 ? [10] : [13] [10]: t6 = t4[-1]; [11] [11]: t6 == 43 ? [12] : [13] [12]: leaf `case ([.., 42], [.., 43]):` -[13]: t2 == 1 ? [14] : [22] -[14]: t7 = t1[0]; [15] -[15]: t7 <-- t3; [16] -[16]: t5 == 1 ? [17] : [22] -[17]: t9 = t4[0]; [18] -[18]: t9 <-- t6; [22] -[19]: t2 == 1 ? [20] : [22] -[20]: t7 = t1[0]; [21] -[21]: t7 <-- t3; [22] -[22]: leaf `switch (a, b) +[13]: leaf `switch (a, b) { case ([.., 42], [.., 43]): case ([42], [43]): @@ -6471,20 +6452,15 @@ void Test(int[] a) ); VerifyDecisionDagDump(comp, -@"[0]: t0 != null ? [1] : [13] +@"[0]: t0 != null ? [1] : [8] [1]: t1 = t0.Length; [2] -[2]: t1 >= 2 ? [3] : [13] +[2]: t1 >= 2 ? [3] : [8] [3]: t2 = t0[0]; [4] -[4]: t2 == 1 ? [5] : [13] +[4]: t2 == 1 ? [5] : [8] [5]: t3 = t0[-1]; [6] [6]: t3 == 3 ? [7] : [8] [7]: leaf `case [1, .., 3]:` -[8]: t1 == 3 ? [9] : [13] -[9]: t4 = t0[1]; [10] -[10]: t4 == 2 ? [11] : [13] -[11]: t5 = t0[2]; [12] -[12]: t5 <-- t3; [13] -[13]: leaf `switch (a) +[8]: leaf `switch (a) { case [1, .., 3]: case [1, 2, 3]: @@ -6651,24 +6627,23 @@ void Test(object[] a) Diagnostic(ErrorCode.ERR_SwitchCaseSubsumed, "[var unreachable]").WithLocation(17, 18)); VerifyDecisionDagDump(comp, -@"[0]: t0 != null ? [1] : [10] +@"[0]: t0 != null ? [1] : [9] [1]: t1 = t0.Length; [2] -[2]: t1 >= 1 ? [3] : [10] +[2]: t1 >= 1 ? [3] : [9] [3]: t2 = t0[0]; [4] [4]: t2 == null ? [5] : [6] [5]: leaf `case [null, ..]:` -[6]: t3 = t0[-1]; [7] -[7]: t1 == 1 ? [8] : [9] -[8]: t3 <-- t2; [11] -[9]: t3 == null ? [10] : [11] -[10]: leaf `switch (a) +[6]: t1 == 1 ? [10] : [7] +[7]: t3 = t0[-1]; [8] +[8]: t3 == null ? [9] : [10] +[9]: leaf `switch (a) { case [null, ..]: case [.., not null]: case [var unreachable]: break; }` -[11]: leaf `case [.., not null]:` +[10]: leaf `case [.., not null]:` "); } @@ -6701,18 +6676,17 @@ public static void Test(int[] a) CompileAndVerify(comp, expectedOutput: "2"); VerifyDecisionDagDump(comp, -@"[0]: t0 != null ? [1] : [11] +@"[0]: t0 != null ? [1] : [10] [1]: t1 = t0.Length; [2] -[2]: t1 >= 2 ? [3] : [11] +[2]: t1 >= 2 ? [3] : [10] [3]: t2 = t0[1]; [4] [4]: t2 > 0 ? [5] : [6] [5]: leaf `case [_, > 0, ..]:` -[6]: t3 = t0[-2]; [7] -[7]: t1 == 3 ? [8] : [9] -[8]: t3 <-- t2; [10] -[9]: t3 <= 0 ? [10] : [11] -[10]: leaf `case [.., <= 0, _]:` -[11]: leaf `default` +[6]: t1 == 3 ? [9] : [7] +[7]: t3 = t0[-2]; [8] +[8]: t3 <= 0 ? [9] : [10] +[9]: leaf `case [.., <= 0, _]:` +[10]: leaf `default` "); } @@ -6824,18 +6798,15 @@ void Test(int[] a) AssertEx.Multiple( () => VerifyDecisionDagDump(comp, -@"[0]: t0 != null ? [1] : [11] +@"[0]: t0 != null ? [1] : [8] [1]: t1 = t0.Length; [2] -[2]: t1 == 1 ? [3] : [10] +[2]: t1 == 1 ? [3] : [7] [3]: t2 = t0[0]; [4] [4]: t2 < 0 ? [5] : [6] [5]: leaf `[<0, ..] => 0` -[6]: t3 = DagSliceEvaluation(t0); [7] -[7]: t4 = t3.Length; [8] -[8]: t5 = t3[0]; [9] -[9]: leaf `[..[>= 0]] or [..null] => 1` -[10]: leaf `{ Length: not 1 } => 0` -[11]: leaf `a switch +[6]: leaf `[..[>= 0]] or [..null] => 1` +[7]: leaf `{ Length: not 1 } => 0` +[8]: leaf `a switch { { Length: not 1 } => 0, [<0, ..] => 0, @@ -6845,18 +6816,15 @@ void Test(int[] a) ", index: 0), () => VerifyDecisionDagDump(comp, -@"[0]: t0 != null ? [1] : [11] +@"[0]: t0 != null ? [1] : [8] [1]: t1 = t0.Length; [2] -[2]: t1 == 1 ? [3] : [10] +[2]: t1 == 1 ? [3] : [7] [3]: t2 = t0[0]; [4] [4]: t2 < 0 ? [5] : [6] [5]: leaf `[<0, ..] => 0` -[6]: t3 = DagSliceEvaluation(t0); [7] -[7]: t4 = t3.Length; [8] -[8]: t5 = t3[0]; [9] -[9]: leaf `[..[>= 0]] => 1` -[10]: leaf `{ Length: not 1 } => 0` -[11]: leaf `a switch +[6]: leaf `[..[>= 0]] => 1` +[7]: leaf `{ Length: not 1 } => 0` +[8]: leaf `a switch { { Length: not 1 } => 0, [<0, ..] => 0, @@ -6892,22 +6860,21 @@ void Test(int[] a) Diagnostic(ErrorCode.ERR_SwitchArmSubsumed, "[var unreachable]").WithLocation(12, 13)); VerifyDecisionDagDump(comp, -@"[0]: t0 != null ? [1] : [15] +@"[0]: t0 != null ? [1] : [14] [1]: t1 = t0.Length; [2] -[2]: t1 >= 1 ? [3] : [14] +[2]: t1 >= 1 ? [3] : [13] [3]: t2 = t0[-1]; [4] [4]: t2 > 0 ? [5] : [6] [5]: leaf `[.., >0] => 1` -[6]: t3 = t0[0]; [7] -[7]: t1 == 1 ? [8] : [10] -[8]: t3 <-- t2; [9] -[9]: t3 < 0 ? [11] : [13] -[10]: t3 < 0 ? [11] : [12] -[11]: leaf `[<0, ..] => 2` -[12]: t3 == 0 ? [13] : [14] -[13]: leaf `[0, ..] => 3` -[14]: leaf `{ Length: not 1 } => 4` -[15]: leaf `a switch +[6]: t1 == 1 ? [7] : [8] +[7]: t2 < 0 ? [10] : [12] +[8]: t3 = t0[0]; [9] +[9]: t3 < 0 ? [10] : [11] +[10]: leaf `[<0, ..] => 2` +[11]: t3 == 0 ? [12] : [13] +[12]: leaf `[0, ..] => 3` +[13]: leaf `{ Length: not 1 } => 4` +[14]: leaf `a switch { [.., >0] => 1, [<0, ..] => 2, @@ -6969,33 +6936,17 @@ void Test(int[][] a) Diagnostic(ErrorCode.ERR_SwitchCaseSubsumed, "[[42]]").WithLocation(9, 18)); VerifyDecisionDagDump(comp, -@"[0]: t0 != null ? [1] : [26] +@"[0]: t0 != null ? [1] : [10] [1]: t1 = t0.Length; [2] -[2]: t1 >= 1 ? [3] : [26] +[2]: t1 >= 1 ? [3] : [10] [3]: t2 = t0[-1]; [4] -[4]: t2 != null ? [5] : [23] +[4]: t2 != null ? [5] : [10] [5]: t3 = t2.Length; [6] -[6]: t3 >= 1 ? [7] : [18] +[6]: t3 >= 1 ? [7] : [10] [7]: t4 = t2[-1]; [8] [8]: t4 == 42 ? [9] : [10] [9]: leaf `case [.., [.., 42]]:` -[10]: t1 == 1 ? [11] : [26] -[11]: t5 = t0[0]; [12] -[12]: t5 <-- t2; [13] -[13]: t7 = t5.Length; [14] -[14]: t7 <-- t3; [15] -[15]: t7 == 1 ? [16] : [26] -[16]: t9 = t5[0]; [17] -[17]: t9 <-- t4; [26] -[18]: t1 == 1 ? [19] : [26] -[19]: t5 = t0[0]; [20] -[20]: t5 <-- t2; [21] -[21]: t7 = t5.Length; [22] -[22]: t7 <-- t3; [26] -[23]: t1 == 1 ? [24] : [26] -[24]: t5 = t0[0]; [25] -[25]: t5 <-- t2; [26] -[26]: leaf `switch (a) +[10]: leaf `switch (a) { case [.., [.., 42]]: case [[42]]: @@ -7294,21 +7245,20 @@ void Test(int[] a) // case [_]: Diagnostic(ErrorCode.ERR_SwitchCaseSubsumed, "[_]").WithLocation(11, 18)); VerifyDecisionDagDump(comp, -@"[0]: t0 != null ? [1] : [14] +@"[0]: t0 != null ? [1] : [13] [1]: t1 = t0.Length; [2] -[2]: t1 >= 1 ? [3] : [14] +[2]: t1 >= 1 ? [3] : [13] [3]: t2 = t0[-1]; [4] [4]: t2 == 0 ? [5] : [6] [5]: leaf `case [.., 0]:` -[6]: t3 = t0[0]; [7] -[7]: t1 == 1 ? [8] : [10] -[8]: t3 <-- t2; [9] -[9]: t3 < 0 ? [11] : [13] -[10]: t3 < 0 ? [11] : [12] -[11]: leaf `case [<0, ..]:` -[12]: t2 > 0 ? [13] : [14] -[13]: leaf `case [.., >0]:` -[14]: leaf `switch (a) +[6]: t1 == 1 ? [7] : [8] +[7]: t2 < 0 ? [10] : [12] +[8]: t3 = t0[0]; [9] +[9]: t3 < 0 ? [10] : [11] +[10]: leaf `case [<0, ..]:` +[11]: t2 > 0 ? [12] : [13] +[12]: leaf `case [.., >0]:` +[13]: leaf `switch (a) { case [.., 0]: case [<0, ..]: @@ -9684,4 +9634,157 @@ void M() Patterns (0) """); } + + [Fact] + [WorkItem("https://github.com/dotnet/roslyn/issues/82398")] + public void IndexerEvaluation_DifferentResultTypes() + { + var source = """ + using System; + + class CollectionA + { + public int Length => 3; + public int this[int i] => 101; + public CollectionB Slice(int start, int length) => new CollectionB(); + } + + class CollectionB + { + public int Length => 1; + public string this[int i] => "102"; + } + + class Program + { + static bool Test(CollectionA a1) + { + return a1 is [100, ..] or [.. ["102", ..]]; + } + + static void Main() + { + var a1 = new CollectionA(); + Console.WriteLine(Test(a1)); + } + } + """; + + var compilation = CreateCompilationWithIndexAndRange(source, options: TestOptions.DebugExe); + + VerifyDecisionDagDump(compilation, +@"[0]: t0 != null ? [1] : [10] +[1]: t1 = t0.Length; [2] +[2]: t1 >= 1 ? [3] : [10] +[3]: t2 = t0[0]; [4] +[4]: t2 == 100 ? [9] : [5] +[5]: t3 = DagSliceEvaluation(t0); [6] +[6]: t4 = t3.Length; [7] +[7]: t5 = t3[0]; [8] +[8]: t5 == ""102"" ? [9] : [10] +[9]: leaf `[100, ..] or [.. [""102"", ..]]` +[10]: leaf `[100, ..] or [.. [""102"", ..]]` +"); + + var verifier = CompileAndVerify(compilation, expectedOutput: "True").VerifyDiagnostics(); + + verifier.VerifyIL("Program.Test", @" +{ + // Code size 84 (0x54) + .maxstack 3 + .locals init (int V_0, + int V_1, + CollectionB V_2, + string V_3, + bool V_4, + bool V_5) + IL_0000: nop + IL_0001: ldarg.0 + IL_0002: brfalse.s IL_0048 + IL_0004: ldarg.0 + IL_0005: callvirt ""int CollectionA.Length.get"" + IL_000a: stloc.0 + IL_000b: ldloc.0 + IL_000c: ldc.i4.1 + IL_000d: blt.s IL_0048 + IL_000f: ldarg.0 + IL_0010: ldc.i4.0 + IL_0011: callvirt ""int CollectionA.this[int].get"" + IL_0016: stloc.1 + IL_0017: ldloc.1 + IL_0018: ldc.i4.s 100 + IL_001a: beq.s IL_0043 + IL_001c: ldarg.0 + IL_001d: ldc.i4.0 + IL_001e: ldloc.0 + IL_001f: callvirt ""CollectionB CollectionA.Slice(int, int)"" + IL_0024: stloc.2 + IL_0025: ldloc.2 + IL_0026: callvirt ""int CollectionB.Length.get"" + IL_002b: pop + IL_002c: ldloc.2 + IL_002d: ldc.i4.0 + IL_002e: callvirt ""string CollectionB.this[int].get"" + IL_0033: stloc.3 + IL_0034: ldloc.3 + IL_0035: ldstr ""102"" + IL_003a: call ""bool string.op_Equality(string, string)"" + IL_003f: brtrue.s IL_0043 + IL_0041: br.s IL_0048 + IL_0043: ldc.i4.1 + IL_0044: stloc.s V_4 + IL_0046: br.s IL_004b + IL_0048: ldc.i4.0 + IL_0049: stloc.s V_4 + IL_004b: ldloc.s V_4 + IL_004d: stloc.s V_5 + IL_004f: br.s IL_0051 + IL_0051: ldloc.s V_5 + IL_0053: ret +} +"); + } + + [Fact] + public void SliceStart_01() + { + // Span + var source = """ +class C +{ + static bool M(System.Span x) + { + return x is [_, .. var rest]; + } +} +"""; + + var comp = CreateCompilation(source, targetFramework: TargetFramework.Net100); + + var verifier = CompileAndVerify(comp, verify: Verification.Skipped).VerifyDiagnostics(); + verifier.VerifyIL("C.M", """ +{ + // Code size 28 (0x1c) + .maxstack 4 + .locals init (int V_0) + IL_0000: ldarga.s V_0 + IL_0002: call "int System.Span.Length.get" + IL_0007: stloc.0 + IL_0008: ldloc.0 + IL_0009: ldc.i4.1 + IL_000a: blt.s IL_001a + IL_000c: ldarga.s V_0 + IL_000e: ldc.i4.1 + IL_000f: ldloc.0 + IL_0010: ldc.i4.1 + IL_0011: sub + IL_0012: call "System.Span System.Span.Slice(int, int)" + IL_0017: pop + IL_0018: ldc.i4.1 + IL_0019: ret + IL_001a: ldc.i4.0 + IL_001b: ret +} +"""); + } } diff --git a/src/Compilers/CSharp/Test/Emit3/Semantics/RecordTests.cs b/src/Compilers/CSharp/Test/Emit3/Semantics/RecordTests.cs index 6701cd729597..8657506ef28b 100644 --- a/src/Compilers/CSharp/Test/Emit3/Semantics/RecordTests.cs +++ b/src/Compilers/CSharp/Test/Emit3/Semantics/RecordTests.cs @@ -24368,9 +24368,6 @@ record B : A; "; comp = CreateEmptyCompilation(source1, references: new[] { ref0 }, parseOptions: TestOptions.Regular9.WithNoRefSafetyRulesAttribute()); comp.VerifyDiagnostics( - // (1,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported - // record A; - Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.IEquatable`1").WithLocation(1, 8), // (1,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported // record A; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.IEquatable`1").WithLocation(1, 8), @@ -24380,9 +24377,6 @@ record B : A; // (2,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported // record B : A; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "B").WithArguments("System.IEquatable`1").WithLocation(2, 8), - // (2,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported - // record B : A; - Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "B").WithArguments("System.IEquatable`1").WithLocation(2, 8), // (2,8): error CS0518: Predefined type 'System.Type' is not defined or imported // record B : A; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "B").WithArguments("System.Type").WithLocation(2, 8) @@ -24433,9 +24427,6 @@ record B : A, System.IEquatable; "; comp = CreateEmptyCompilation(source1, references: new[] { ref0 }, parseOptions: TestOptions.Regular9.WithNoRefSafetyRulesAttribute()); comp.VerifyDiagnostics( - // (1,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported - // record A : System.IEquatable>; - Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.IEquatable`1").WithLocation(1, 8), // (1,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported // record A : System.IEquatable>; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.IEquatable`1").WithLocation(1, 8), @@ -24448,9 +24439,6 @@ record B : A, System.IEquatable; // (2,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported // record B : A, System.IEquatable; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "B").WithArguments("System.IEquatable`1").WithLocation(2, 8), - // (2,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported - // record B : A, System.IEquatable; - Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "B").WithArguments("System.IEquatable`1").WithLocation(2, 8), // (2,8): error CS0518: Predefined type 'System.Type' is not defined or imported // record B : A, System.IEquatable; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "B").WithArguments("System.Type").WithLocation(2, 8), @@ -24510,9 +24498,6 @@ record B : A, IEquatable; // (2,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported // record A : IEquatable>; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.IEquatable`1").WithLocation(2, 8), - // (2,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported - // record A : IEquatable>; - Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.IEquatable`1").WithLocation(2, 8), // (2,8): error CS0518: Predefined type 'System.Type' is not defined or imported // record A : IEquatable>; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.Type").WithLocation(2, 8), @@ -24522,9 +24507,6 @@ record B : A, IEquatable; // (3,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported // record B : A, IEquatable; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "B").WithArguments("System.IEquatable`1").WithLocation(3, 8), - // (3,8): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported - // record B : A, IEquatable; - Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "B").WithArguments("System.IEquatable`1").WithLocation(3, 8), // (3,8): error CS0518: Predefined type 'System.Type' is not defined or imported // record B : A, IEquatable; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "B").WithArguments("System.Type").WithLocation(3, 8), diff --git a/src/Compilers/CSharp/Test/EndToEnd/EndToEndTests.cs b/src/Compilers/CSharp/Test/EndToEnd/EndToEndTests.cs index 6871cc0f10ec..80fd5c05c364 100644 --- a/src/Compilers/CSharp/Test/EndToEnd/EndToEndTests.cs +++ b/src/Compilers/CSharp/Test/EndToEnd/EndToEndTests.cs @@ -892,7 +892,7 @@ public void ManyBinaryPatterns_01(string pattern, string expectedOutput) public void ManyBinaryPatterns_02() { const int numOfEnumMembers = 5_000; - const int capacity = 97973; + var capacity = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 97973 : 87960; var builder = new StringBuilder(capacity); @@ -972,7 +972,7 @@ enum E public void ManyBinaryPatterns_03() { const int numOfEnumMembers = 4_000; - const int capacity = 47065; + var capacity = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 47065 : 43055; var builder = new StringBuilder(capacity); diff --git a/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_IInterpolatedStringOperation.cs b/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_IInterpolatedStringOperation.cs index 878d2d558f1f..804f7794b685 100644 --- a/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_IInterpolatedStringOperation.cs +++ b/src/Compilers/CSharp/Test/IOperation/IOperation/IOperationTests_IInterpolatedStringOperation.cs @@ -4,6 +4,7 @@ #nullable disable +using System.Linq; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Microsoft.CodeAnalysis.Test.Utilities; @@ -6454,5 +6455,414 @@ public CustomHandler(int literalLength, int formattedCount, out int i) : this(li "; VerifyFlowGraphAndDiagnosticsForTest(new[] { code, InterpolatedStringHandlerArgumentAttribute, handler }, expectedFlowGraph, expectedDiagnostics); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82273")] + public void ObjectInitializerWithMissingAccessor_01() + { + var code = """ +public class Program +{ + public static void Main() + { + /**/ + _ = new C() { [$"{1}"] = 1 }; + /**/ + } +} + +public class C +{ + public int this[[System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] CustomHandler h] + { + get => throw null!; + // lacks setter + } +} + +[System.Runtime.CompilerServices.InterpolatedStringHandler] +public struct CustomHandler +{ + public CustomHandler(int literalLength, int formattedCount, C c) + { + } + + public void AppendFormatted(int i) {} +} +"""; + + DiagnosticDescription[] expectedDiagnostics = [ + // (6,23): error CS0200: Property or indexer 'C.this[CustomHandler]' cannot be assigned to -- it is read only + // _ = new C() { [$"{1}"] = 1 }; + Diagnostic(ErrorCode.ERR_AssgReadonlyProp, @"[$""{1}""]").WithArguments("C.this[CustomHandler]").WithLocation(6, 23), + // (6,24): error CS8976: Interpolated string handler conversions that reference the instance being indexed cannot be used in indexer member initializers. + // _ = new C() { [$"{1}"] = 1 }; + Diagnostic(ErrorCode.ERR_InterpolatedStringsReferencingInstanceCannotBeInObjectInitializers, @"$""{1}""").WithLocation(6, 24) + ]; + + var comp = CreateCompilation(code, targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics(expectedDiagnostics); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var mainDeclaration = tree.GetRoot().DescendantNodes().OfType().First(); + + var (graph, symbol) = ControlFlowGraphVerifier.GetControlFlowGraph(mainDeclaration.Body, model); + ControlFlowGraphVerifier.VerifyGraph(comp, """ +Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} +.locals {R1} +{ + CaptureIds: [0] + Block[B1] - Block + Predecessors: [B0] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsInvalid, IsImplicit) (Syntax: 'new C() { [$"{1}"] = 1 }') + Value: + IObjectCreationOperation (Constructor: C..ctor()) (OperationKind.ObjectCreation, Type: C, IsInvalid) (Syntax: 'new C() { [$"{1}"] = 1 }') + Arguments(0) + Initializer: + null + Next (Regular) Block[B2] + Entering: {R2} + .locals {R2} + { + CaptureIds: [1] + Block[B2] - Block + Predecessors: [B1] + Statements (3) + IFlowCaptureOperation: 1 (OperationKind.FlowCapture, Type: null, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Value: + IObjectCreationOperation (Constructor: CustomHandler..ctor(System.Int32 literalLength, System.Int32 formattedCount, C c)) (OperationKind.ObjectCreation, Type: CustomHandler, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Arguments(3): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: literalLength) (OperationKind.Argument, Type: null, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 0, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: formattedCount) (OperationKind.Argument, Type: null, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: c) (OperationKind.Argument, Type: null, IsImplicit) (Syntax: 'C') + IInvalidOperation (OperationKind.Invalid, Type: null, IsImplicit) (Syntax: 'C') + Children(0) + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Initializer: + null + IInvocationOperation ( void CustomHandler.AppendFormatted(System.Int32 i)) (OperationKind.Invocation, Type: System.Void, IsInvalid, IsImplicit) (Syntax: '{1}') + Instance Receiver: + IFlowCaptureReferenceOperation: 1 (OperationKind.FlowCaptureReference, Type: CustomHandler, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Arguments(1): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: i) (OperationKind.Argument, Type: null, IsInvalid, IsImplicit) (Syntax: '1') + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1, IsInvalid) (Syntax: '1') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Int32, IsInvalid) (Syntax: '[$"{1}"] = 1') + Left: + IInvalidOperation (OperationKind.Invalid, Type: System.Int32, IsInvalid) (Syntax: '[$"{1}"]') + Children(1): + IFlowCaptureReferenceOperation: 1 (OperationKind.FlowCaptureReference, Type: CustomHandler, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Right: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1) (Syntax: '1') + Next (Regular) Block[B3] + Leaving: {R2} + } + Block[B3] - Block + Predecessors: [B2] + Statements (1) + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null, IsInvalid) (Syntax: '_ = new C() ... 1}"] = 1 };') + Expression: + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: C, IsInvalid) (Syntax: '_ = new C() ... {1}"] = 1 }') + Left: + IDiscardOperation (Symbol: C _) (OperationKind.Discard, Type: C) (Syntax: '_') + Right: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: C, IsInvalid, IsImplicit) (Syntax: 'new C() { [$"{1}"] = 1 }') + Next (Regular) Block[B4] + Leaving: {R1} +} +Block[B4] - Exit + Predecessors: [B3] + Statements (0) +""", graph, symbol); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82273")] + public void ObjectInitializerWithMissingAccessor_02() + { + // missing setter in nested object initializer + var code = """ +public class Program +{ + public static void Main() + { + /**/ + _ = new D() { Property = { [$"{1}"] = 1 } }; + /**/ + } +} + +public class D +{ + public C Property => throw null!; +} + +public class C +{ + public int this[[System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] CustomHandler h] + { + get => throw null!; + // lacks setter + } +} + +[System.Runtime.CompilerServices.InterpolatedStringHandler] +public struct CustomHandler +{ + public CustomHandler(int literalLength, int formattedCount, C c) + { + } + + public void AppendFormatted(int i) {} +} +"""; + + DiagnosticDescription[] expectedDiagnostics = [ + // (6,36): error CS0200: Property or indexer 'C.this[CustomHandler]' cannot be assigned to -- it is read only + // _ = new D() { Property = { [$"{1}"] = 1 } }; + Diagnostic(ErrorCode.ERR_AssgReadonlyProp, @"[$""{1}""]").WithArguments("C.this[CustomHandler]").WithLocation(6, 36), + // (6,37): error CS8976: Interpolated string handler conversions that reference the instance being indexed cannot be used in indexer member initializers. + // _ = new D() { Property = { [$"{1}"] = 1 } }; + Diagnostic(ErrorCode.ERR_InterpolatedStringsReferencingInstanceCannotBeInObjectInitializers, @"$""{1}""").WithLocation(6, 37) + ]; + + var comp = CreateCompilation(code, targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics(expectedDiagnostics); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var mainDeclaration = tree.GetRoot().DescendantNodes().OfType().First(); + + var (graph, symbol) = ControlFlowGraphVerifier.GetControlFlowGraph(mainDeclaration.Body, model); + ControlFlowGraphVerifier.VerifyGraph(comp, """ +Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} +.locals {R1} +{ + CaptureIds: [0] + Block[B1] - Block + Predecessors: [B0] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsInvalid, IsImplicit) (Syntax: 'new D() { P ... }"] = 1 } }') + Value: + IObjectCreationOperation (Constructor: D..ctor()) (OperationKind.ObjectCreation, Type: D, IsInvalid) (Syntax: 'new D() { P ... }"] = 1 } }') + Arguments(0) + Initializer: + null + Next (Regular) Block[B2] + Entering: {R2} + .locals {R2} + { + CaptureIds: [1] + Block[B2] - Block + Predecessors: [B1] + Statements (3) + IFlowCaptureOperation: 1 (OperationKind.FlowCapture, Type: null, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Value: + IObjectCreationOperation (Constructor: CustomHandler..ctor(System.Int32 literalLength, System.Int32 formattedCount, C c)) (OperationKind.ObjectCreation, Type: CustomHandler, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Arguments(3): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: literalLength) (OperationKind.Argument, Type: null, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 0, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: formattedCount) (OperationKind.Argument, Type: null, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: c) (OperationKind.Argument, Type: null, IsImplicit) (Syntax: 'Property') + IInvalidOperation (OperationKind.Invalid, Type: null, IsImplicit) (Syntax: 'Property') + Children(0) + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Initializer: + null + IInvocationOperation ( void CustomHandler.AppendFormatted(System.Int32 i)) (OperationKind.Invocation, Type: System.Void, IsInvalid, IsImplicit) (Syntax: '{1}') + Instance Receiver: + IFlowCaptureReferenceOperation: 1 (OperationKind.FlowCaptureReference, Type: CustomHandler, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Arguments(1): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: i) (OperationKind.Argument, Type: null, IsInvalid, IsImplicit) (Syntax: '1') + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1, IsInvalid) (Syntax: '1') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Int32, IsInvalid) (Syntax: '[$"{1}"] = 1') + Left: + IInvalidOperation (OperationKind.Invalid, Type: System.Int32, IsInvalid) (Syntax: '[$"{1}"]') + Children(1): + IFlowCaptureReferenceOperation: 1 (OperationKind.FlowCaptureReference, Type: CustomHandler, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Right: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1) (Syntax: '1') + Next (Regular) Block[B3] + Leaving: {R2} + } + Block[B3] - Block + Predecessors: [B2] + Statements (1) + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null, IsInvalid) (Syntax: '_ = new D() ... "] = 1 } };') + Expression: + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: D, IsInvalid) (Syntax: '_ = new D() ... }"] = 1 } }') + Left: + IDiscardOperation (Symbol: D _) (OperationKind.Discard, Type: D) (Syntax: '_') + Right: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: D, IsInvalid, IsImplicit) (Syntax: 'new D() { P ... }"] = 1 } }') + Next (Regular) Block[B4] + Leaving: {R1} +} +Block[B4] - Exit + Predecessors: [B3] + Statements (0) +""", graph, symbol); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82273")] + public void ObjectInitializerWithMissingAccessor_03() + { + var code = """ +public class Program +{ + public static void Main() + { + /**/ + _ = new C() { [$"{1}"] = { Property = 1 } }; + /**/ + } +} + +public class D +{ + public int Property { set { } } +} + +public class C +{ + public D this[[System.Runtime.CompilerServices.InterpolatedStringHandlerArgument("")] CustomHandler h] + { + // missing getter + set => throw null!; + } +} + +[System.Runtime.CompilerServices.InterpolatedStringHandler] +public struct CustomHandler +{ + public CustomHandler(int literalLength, int formattedCount, C c) + { + } + + public void AppendFormatted(int i) {} +} +"""; + + DiagnosticDescription[] expectedDiagnostics = [ + // (6,23): error CS0154: The property or indexer 'C.this[CustomHandler]' cannot be used in this context because it lacks the get accessor + // _ = new C() { [$"{1}"] = { Property = 1 } }; + Diagnostic(ErrorCode.ERR_PropertyLacksGet, @"[$""{1}""]").WithArguments("C.this[CustomHandler]").WithLocation(6, 23), + // (6,24): error CS8976: Interpolated string handler conversions that reference the instance being indexed cannot be used in indexer member initializers. + // _ = new C() { [$"{1}"] = { Property = 1 } }; + Diagnostic(ErrorCode.ERR_InterpolatedStringsReferencingInstanceCannotBeInObjectInitializers, @"$""{1}""").WithLocation(6, 24) + ]; + + var comp = CreateCompilation(code, targetFramework: TargetFramework.NetCoreApp); + comp.VerifyDiagnostics(expectedDiagnostics); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var mainDeclaration = tree.GetRoot().DescendantNodes().OfType().First(); + + var (graph, symbol) = ControlFlowGraphVerifier.GetControlFlowGraph(mainDeclaration.Body, model); + ControlFlowGraphVerifier.VerifyGraph(comp, """ +Block[B0] - Entry + Statements (0) + Next (Regular) Block[B1] + Entering: {R1} +.locals {R1} +{ + CaptureIds: [0] + Block[B1] - Block + Predecessors: [B0] + Statements (1) + IFlowCaptureOperation: 0 (OperationKind.FlowCapture, Type: null, IsInvalid, IsImplicit) (Syntax: 'new C() { [ ... rty = 1 } }') + Value: + IObjectCreationOperation (Constructor: C..ctor()) (OperationKind.ObjectCreation, Type: C, IsInvalid) (Syntax: 'new C() { [ ... rty = 1 } }') + Arguments(0) + Initializer: + null + Next (Regular) Block[B2] + Entering: {R2} + .locals {R2} + { + CaptureIds: [1] + Block[B2] - Block + Predecessors: [B1] + Statements (3) + IFlowCaptureOperation: 1 (OperationKind.FlowCapture, Type: null, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Value: + IObjectCreationOperation (Constructor: CustomHandler..ctor(System.Int32 literalLength, System.Int32 formattedCount, C c)) (OperationKind.ObjectCreation, Type: CustomHandler, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Arguments(3): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: literalLength) (OperationKind.Argument, Type: null, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 0, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: formattedCount) (OperationKind.Argument, Type: null, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: c) (OperationKind.Argument, Type: null, IsImplicit) (Syntax: 'C') + IInvalidOperation (OperationKind.Invalid, Type: null, IsImplicit) (Syntax: 'C') + Children(0) + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + Initializer: + null + IInvocationOperation ( void CustomHandler.AppendFormatted(System.Int32 i)) (OperationKind.Invocation, Type: System.Void, IsInvalid, IsImplicit) (Syntax: '{1}') + Instance Receiver: + IFlowCaptureReferenceOperation: 1 (OperationKind.FlowCaptureReference, Type: CustomHandler, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Arguments(1): + IArgumentOperation (ArgumentKind.Explicit, Matching Parameter: i) (OperationKind.Argument, Type: null, IsInvalid, IsImplicit) (Syntax: '1') + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1, IsInvalid) (Syntax: '1') + InConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + OutConversion: CommonConversion (Exists: True, IsIdentity: True, IsNumeric: False, IsReference: False, IsUserDefined: False) (MethodSymbol: null) + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: System.Int32) (Syntax: 'Property = 1') + Left: + IPropertyReferenceOperation: System.Int32 D.Property { set; } (OperationKind.PropertyReference, Type: System.Int32) (Syntax: 'Property') + Instance Receiver: + IInvalidOperation (OperationKind.Invalid, Type: D, IsInvalid) (Syntax: '[$"{1}"]') + Children(1): + IFlowCaptureReferenceOperation: 1 (OperationKind.FlowCaptureReference, Type: CustomHandler, IsInvalid, IsImplicit) (Syntax: '$"{1}"') + Right: + ILiteralOperation (OperationKind.Literal, Type: System.Int32, Constant: 1) (Syntax: '1') + Next (Regular) Block[B3] + Leaving: {R2} + } + Block[B3] - Block + Predecessors: [B2] + Statements (1) + IExpressionStatementOperation (OperationKind.ExpressionStatement, Type: null, IsInvalid) (Syntax: '_ = new C() ... ty = 1 } };') + Expression: + ISimpleAssignmentOperation (OperationKind.SimpleAssignment, Type: C, IsInvalid) (Syntax: '_ = new C() ... rty = 1 } }') + Left: + IDiscardOperation (Symbol: C _) (OperationKind.Discard, Type: C) (Syntax: '_') + Right: + IFlowCaptureReferenceOperation: 0 (OperationKind.FlowCaptureReference, Type: C, IsInvalid, IsImplicit) (Syntax: 'new C() { [ ... rty = 1 } }') + Next (Regular) Block[B4] + Leaving: {R1} +} +Block[B4] - Exit + Predecessors: [B3] + Statements (0) +""", graph, symbol); + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/AwaitExpressionTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/AwaitExpressionTests.cs index 35e4872bcf4d..cd18932d25c3 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/AwaitExpressionTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/AwaitExpressionTests.cs @@ -310,6 +310,86 @@ public C() { info.RuntimeAwaitMethod.ToTestDisplayString()); } + [Fact] + public void TestAwaitInfo_RuntimeAsync_NullableTaskOperand() + { + var text = """ + #nullable enable + using System.Threading.Tasks; + + await M(null); + + Task? M(object o) => throw null!; + """; + + var comp = CreateRuntimeAsyncCompilation(text); + comp.VerifyDiagnostics( + // (4,7): warning CS8604: Possible null reference argument for parameter 'task' in 'void AsyncHelpers.Await(Task task)'. + // await M(null); + Diagnostic(ErrorCode.WRN_NullReferenceArgument, "M(null)").WithArguments("task", "void AsyncHelpers.Await(Task task)").WithLocation(4, 7), + // (4,9): warning CS8625: Cannot convert null literal to non-nullable reference type. + // await M(null); + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(4, 9)); + + var tree = comp.SyntaxTrees[0]; + var syntaxNode = (AwaitExpressionSyntax)tree.FindNodeOrTokenByKind(SyntaxKind.AwaitExpression).AsNode(); + var treeModel = comp.GetSemanticModel(tree); + var info = treeModel.GetAwaitExpressionInfo(syntaxNode); + + Assert.Null(info.GetAwaiterMethod); + Assert.Null(info.GetResultMethod); + Assert.Null(info.IsCompletedProperty); + AssertEx.Equal("void System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.Task! task)", info.RuntimeAwaitMethod.ToTestDisplayString(includeNonNullable: true)); + } + + [Fact] + public void TestAwaitInfo_RuntimeAsync_NullableTaskLikeAwaiter() + { + var text = """ + #nullable enable + using System; + using System.Runtime.CompilerServices; + + await M(null); + + MyTask M(object o) => throw null!; + + public class MyTask + { + public MyAwaiter? GetAwaiter() => throw null!; + } + + public class MyAwaiter : ICriticalNotifyCompletion + { + public bool IsCompleted => false; + public void GetResult() { } + public void OnCompleted(Action continuation) { } + public void UnsafeOnCompleted(Action continuation) { } + } + """; + + var comp = CreateRuntimeAsyncCompilation(text); + comp.VerifyDiagnostics( + // (5,7): warning CS8631: The type 'MyAwaiter?' cannot be used as type parameter 'TAwaiter' in the generic type or method 'AsyncHelpers.UnsafeAwaitAwaiter(TAwaiter)'. Nullability of type argument 'MyAwaiter?' doesn't match constraint type 'System.Runtime.CompilerServices.ICriticalNotifyCompletion'. + // await M(null); + Diagnostic(ErrorCode.WRN_NullabilityMismatchInTypeParameterConstraint, "M").WithArguments("System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiter(TAwaiter)", "System.Runtime.CompilerServices.ICriticalNotifyCompletion", "TAwaiter", "MyAwaiter?").WithLocation(5, 7), + // (5,9): warning CS8625: Cannot convert null literal to non-nullable reference type. + // await M(null); + Diagnostic(ErrorCode.WRN_NullAsNonNullable, "null").WithLocation(5, 9)); + + var tree = comp.SyntaxTrees[0]; + var syntaxNode = (AwaitExpressionSyntax)tree.FindNodeOrTokenByKind(SyntaxKind.AwaitExpression).AsNode(); + var treeModel = comp.GetSemanticModel(tree); + var info = treeModel.GetAwaitExpressionInfo(syntaxNode); + + AssertEx.Equal("MyAwaiter? MyTask.GetAwaiter()", info.GetAwaiterMethod.ToTestDisplayString()); + AssertEx.Equal("void MyAwaiter.GetResult()", info.GetResultMethod.ToTestDisplayString()); + AssertEx.Equal("System.Boolean MyAwaiter.IsCompleted { get; }", info.IsCompletedProperty.ToTestDisplayString()); + AssertEx.Equal("void System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiter(MyAwaiter? awaiter)", info.RuntimeAwaitMethod.ToTestDisplayString(includeNonNullable: true)); + Assert.Equal(global::Microsoft.CodeAnalysis.NullableAnnotation.Annotated, info.RuntimeAwaitMethod.TypeArguments.Single().NullableAnnotation); + Assert.Equal(global::Microsoft.CodeAnalysis.NullableAnnotation.Annotated, info.RuntimeAwaitMethod.Parameters.Single().Type.NullableAnnotation); + } + [Fact] [WorkItem(744146, "https://devdiv.visualstudio.com/DevDiv/_workitems/edit/744146")] public void DefaultAwaitExpressionInfo() diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/DeconstructionTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/DeconstructionTests.cs index c404a79c2a1d..5e7f716ee4a6 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/DeconstructionTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/DeconstructionTests.cs @@ -6608,5 +6608,30 @@ public void ErrorForeachVariable_02() Diagnostic(ErrorCode.WRN_UnreferencedLocalFunction, "m").WithArguments("m").WithLocation(9, 6) ); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/68979")] + public void GetDeconstructionInfo_01() + { + string source = """ +var c1 = new C(); +var c2 = new C(); +c1 = (_, _) = c2; + +class C +{ + public void Deconstruct(out int a, out string b) => throw null; + public static implicit operator C((int i, string s) tuple) => throw null; +} +"""; + + var comp = CreateCompilation(source); + comp.VerifyDiagnostics(); + + var tree = comp.SyntaxTrees[0]; + var model = comp.GetSemanticModel(tree); + var discardAssignment = GetSyntax(tree, "(_, _) = c2"); + var deconstructionInfo = model.GetDeconstructionInfo(discardAssignment); + Assert.Equal("void C.Deconstruct(out System.Int32 a, out System.String b)", deconstructionInfo.Method.ToTestDisplayString()); + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/FunctionPointerTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/FunctionPointerTests.cs index 8325ac3e7b0d..840b2096cae0 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/FunctionPointerTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/FunctionPointerTests.cs @@ -14,6 +14,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests { + [CompilerTrait(CompilerFeature.Unsafe)] public class FunctionPointerTests : CompilingTestBase { private static CSharpCompilation CreateCompilationWithFunctionPointers(string source, CSharpCompilationOptions? options = null, CSharpParseOptions? parseOptions = null, TargetFramework? targetFramework = null) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs index fa741ae178f6..4a71b75672ec 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/InterceptorsTests.cs @@ -22,16 +22,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Semantics; public class InterceptorsTests : CSharpTestBase { - private static readonly (string text, string path) s_attributesSource = (""" - namespace System.Runtime.CompilerServices; - - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] - public sealed class InterceptsLocationAttribute : Attribute - { - public InterceptsLocationAttribute(string filePath, int line, int character) { } - public InterceptsLocationAttribute(int version, string data) { } - } - """, "attributes.cs"); + private static readonly (string text, string path) s_attributesSource = (TestSources.InterceptsLocationAttribute, "attributes.cs"); private static readonly CSharpParseOptions RegularWithInterceptors = TestOptions.Regular.WithFeature(CodeAnalysis.Feature.InterceptorsNamespaces, "global"); private static readonly CSharpParseOptions RegularPreviewWithInterceptors = TestOptions.RegularPreview.WithFeature(CodeAnalysis.Feature.InterceptorsNamespaces, "global"); diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NameOfTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NameOfTests.cs index 7a73e7f95f15..2b6ce037c61c 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/NameOfTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NameOfTests.cs @@ -3296,5 +3296,94 @@ static R F(bool b) // nameof(r[0]); Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "0").WithLocation(14, 22)); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/81738")] + public void NameofInObjectInitializerImplicitIndexer() + { + CompileAndVerify(""" + using System.Collections.Generic; + class C + { + private readonly Dictionary _map = new(); + public int this[string key] + { + get => _map[key]; + set => _map[key] = value; + } + public int P { get; set; } + } + class Program + { + static void Main() + { + var c = new C { [nameof(C.P)] = 42 }; + System.Console.Write(c[nameof(C.P)]); + } + } + """, expectedOutput: "42").VerifyDiagnostics(); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/81738")] + public void NameofOnObjectInitializerRightHandSide() + { + CompileAndVerify(""" + class C + { + public string P { get; set; } + public string Q { get; set; } + } + class Program + { + static void Main() + { + var c = new C { P = nameof(C.Q) }; + System.Console.Write(c.P); + } + } + """, expectedOutput: "Q").VerifyDiagnostics(); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/82474")] + public void ColorColor_FieldInitializer([CombinatorialValues("", "static")] string modifier) + { + var source = $$""" + #pragma warning disable CS0169, CS0414, CS0649 // unused field + class C + { + string F = nameof(D.M); + D D; + } + class D + { + public {{modifier}} void M() { } + } + """; + CreateCompilation(source, parseOptions: TestOptions.RegularPreview).VerifyEmitDiagnostics(); + CreateCompilation(source, parseOptions: TestOptions.Regular12).VerifyEmitDiagnostics(); + CreateCompilation(source, parseOptions: TestOptions.Regular11).VerifyEmitDiagnostics(); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/82474")] + public void ColorColor_Attribute([CombinatorialValues("", "static")] string modifier) + { + var source = $$""" + #pragma warning disable CS0169 // unused field + [A(nameof(D.M))] class C + { + D D; + } + class D + { + public {{modifier}} void M() { } + } + class A : System.Attribute + { + public A(string s) { } + } + """; + CreateCompilation(source, parseOptions: TestOptions.RegularPreview).VerifyEmitDiagnostics(); + CreateCompilation(source, parseOptions: TestOptions.Regular12).VerifyEmitDiagnostics(); + CreateCompilation(source, parseOptions: TestOptions.Regular11).VerifyEmitDiagnostics(); + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableAwaitTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableAwaitTests.cs new file mode 100644 index 000000000000..1973d1fd838a --- /dev/null +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableAwaitTests.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.CSharp.Semantic.UnitTests.Semantics +{ + using Microsoft.CodeAnalysis.CSharp.UnitTests; + using Roslyn.Test.Utilities; + using Xunit; + + public class NullableAwaitTests : SemanticModelTestBase + { + [Fact] + public void AwaitedNullableTypeAssignedToNonNullable_ProducesCS8600() + { + var src = + @" +#nullable enable +using System.Threading.Tasks; +public class TestClass +{ + public async Task Main() + { + Task GetNullableStringAsync() => Task.FromResult(null); + + string? result = await GetNullableStringAsync(); + string nonNullableString = result; + } +}"; + + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (11,36): warning CS8600: Converting null literal or possible null value to non-nullable type. + // string nonNullableString = result; + Diagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, "result").WithLocation(11, 36)); + } + + [Fact] + public void AwaitResultNullableTypeAssignedToNonNullable_ProducesCS8600() + { + var src = + @" +#nullable enable +using System.Threading.Tasks; +public class TestClass +{ + public void Main() + { + Task GetNullableStringAsync() => Task.FromResult(null); + + string? result = GetNullableStringAsync().Result; + string nonNullableString = result; + } +}"; + + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (11,36): warning CS8600: Converting null literal or possible null value to non-nullable type. + // string nonNullableString = result; + Diagnostic(ErrorCode.WRN_ConvertingNullableToNonNullable, "result").WithLocation(11, 36)); + } + + [Fact] + [WorkItem(76886, "https://github.com/dotnet/roslyn/issues/76886")] + public void AwaitedNullableValueTypeAssignedToNonNullable_ProducesCS8629() + { + var src = + @" +#nullable enable +using System.Threading.Tasks; +public class TestClass +{ + public async Task Main() + { + Task GetNullableIntAsync() => Task.FromResult(null); + + int? result = await GetNullableIntAsync(); + int nonNullableInt = result.Value; + } +}"; + + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (11,30): warning CS8629: Nullable value type may be null. + // int nonNullableInt = result.Value; + Diagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, "result").WithLocation(11, 30)); + } + + [Fact] + public void AwaitResultNullableValueTypeAssignedToNonNullable_ProducesCS8629() + { + var src = + @" +#nullable enable +using System.Threading.Tasks; +public class TestClass +{ + public void Main() + { + Task GetNullableIntAsync() => Task.FromResult(null); + + int? result = GetNullableIntAsync().Result; + int nonNullableInt = result.Value; + } +}"; + + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (11,30): warning CS8629: Nullable value type may be null. + // int nonNullableInt = result.Value; + Diagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, "result").WithLocation(11, 30)); + } + + [Fact] + public void NullableValueTypeAssignedToNonNullable_ProducesCS8629() + { + var src = + @" +#nullable enable +using System.Threading.Tasks; +public class TestClass +{ + public async Task Main() + { + int? result = null; + int nonNullableInt = result.Value; + } +}"; + + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (9,30): warning CS8629: Nullable value type may be null. + // int nonNullableInt = result.Value; + Diagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, "result").WithLocation(9, 30)); + } + + [Fact] + public void Await_GenericValueTypeResult_PreservesNullabilityFlow() + { + var src = + @" +#nullable enable +using System.Collections.Generic; +using System.Threading.Tasks; +class C +{ + public async Task> M(T1 t1, T2 t2) => default; + + public async Task M1() + { + string? s = null; + int? i = null; + var res = await M(s, i); + res.Key.ToString(); // expect warning on receiver of 'ToString()' + _ = res.Value.Value; // warn + } +}"; + + var comp = CreateCompilation(src); + comp.VerifyDiagnostics( + // (14,9): warning CS8602: Dereference of a possibly null reference. + // res.Key.ToString(); // expect warning on receiver of 'ToString()' + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "res.Key").WithLocation(14, 9), + // (15,13): warning CS8629: Nullable value type may be null. + // _ = res.Value.Value; // warn + Diagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, "res.Value").WithLocation(15, 13)); + } + + [Fact] + public void InvalidAwait_GenericAwaitResult_StillProducesNullabilityWarning() + { + var src = + @" +#nullable enable +using System.Collections.Immutable; +using System.Threading.Tasks; +class C +{ + public async Task> M(T value) => [value]; + + public Task M1() + { + string? s = null; + var res = await M(s); // error: 'await' without 'async' + res[0].ToString(); // expect warning on receiver of 'ToString()' + } +}"; + + var comp = CreateCompilation(src, targetFramework: TargetFramework.Net100); + comp.VerifyDiagnostics( + // (12,19): error CS4032: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'. + // var res = await M(s); // error: 'await' without 'async' + Diagnostic(ErrorCode.ERR_BadAwaitWithoutAsyncMethod, "await M(s)").WithArguments("System.Threading.Tasks.Task").WithLocation(12, 19), + // (13,9): warning CS8602: Dereference of a possibly null reference. + // res[0].ToString(); // expect warning on receiver of 'ToString()' + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "res[0]").WithLocation(13, 9), + // (9,17): error CS0161: 'C.M1()': not all code paths return a value + // public Task M1() + Diagnostic(ErrorCode.ERR_ReturnExpected, "M1").WithArguments("C.M1()").WithLocation(9, 17)); + } + } +} diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs index 1600e4891dff..891e539fb208 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/NullableReferenceTypesTests.cs @@ -8596,13 +8596,13 @@ public void NullableOption() var comp = CreateCompilation("", options: WithNullable(NullableContextOptions.Enable), parseOptions: TestOptions.Regular7_3); comp.VerifyDiagnostics( // error CS8630: Invalid 'NullableContextOptions' value: 'Enable' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1) ); comp = CreateCompilation("", options: WithNullable(NullableContextOptions.Warnings), parseOptions: TestOptions.Regular7_3); comp.VerifyDiagnostics( // error CS8630: Invalid 'NullableContextOptions' value: 'Warnings' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("NullableContextOptions", "Warnings", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("NullableContextOptions", "Warnings", "7.3", "8.0").WithLocation(1, 1) ); comp = CreateCompilation("", options: WithNullable(NullableContextOptions.Disable), parseOptions: TestOptions.Regular7_3); @@ -10411,7 +10411,7 @@ class B : A where T : A.I comp = CreateCompilation(new[] { source }, options: WithNullableEnable(), parseOptions: TestOptions.Regular7, skipUsesIsNullable: true); comp.VerifyDiagnostics( // error CS8630: Invalid 'NullableContextOptions' value: 'Enable' for C# 7.0. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.0", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.0", "8.0").WithLocation(1, 1) ); comp = CreateCompilation(new[] { source }, options: WithNullableEnable()); @@ -81868,7 +81868,7 @@ static void G6() where T : U comp = CreateCompilation(new[] { source }, options: WithNullableEnable(), parseOptions: TestOptions.Regular7_3, skipUsesIsNullable: true); comp.VerifyDiagnostics( // error CS8630: Invalid 'NullableContextOptions' value: 'Enable' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1) ); } @@ -83795,9 +83795,6 @@ static void F((B?, B) x, (B, B?) y, (B, B) z) // (13,13): warning CS8604: Possible null reference argument for parameter 'a' in 'A.implicit operator C(A a)'. // c = x; // (ImplicitTuple)(ImplicitUserDefined)(ImplicitReference) Diagnostic(ErrorCode.WRN_NullReferenceArgument, "x").WithArguments("a", "A.implicit operator C(A a)").WithLocation(13, 13), - // (13,13): warning CS8619: Nullability of reference types in value of type '(B?, B)' doesn't match target type '(C, C?)'. - // c = x; // (ImplicitTuple)(ImplicitUserDefined)(ImplicitReference) - Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "x").WithArguments("(B?, B)", "(C, C?)").WithLocation(13, 13), // (14,13): warning CS8604: Possible null reference argument for parameter 'a' in 'A.implicit operator C(A a)'. // c = y; // (ImplicitTuple)(ImplicitUserDefined)(ImplicitReference) Diagnostic(ErrorCode.WRN_NullReferenceArgument, "y").WithArguments("a", "A.implicit operator C(A a)").WithLocation(14, 13)); @@ -96293,8 +96290,8 @@ static void F() t.Item1.FB.ToString(); // 2 t.Item2.Item1.FA.ToString(); t = ((B, (A, A)))(b, (b, b)); - t.Item1.FB.ToString(); - t.Item2.Item1.FA.ToString(); // 3 + t.Item1.FB.ToString(); // 3 + t.Item2.Item1.FA.ToString(); (A, (B, B)) u; u = t; // 4 u.Item1.FA.ToString(); // 5 @@ -96312,9 +96309,9 @@ static void F() // (16,9): warning CS8602: Dereference of a possibly null reference. // t.Item1.FB.ToString(); // 2 Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "t.Item1.FB").WithLocation(16, 9), - // (20,9): warning CS8602: Dereference of a possibly null reference. - // t.Item2.Item1.FA.ToString(); // 3 - Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "t.Item2.Item1.FA").WithLocation(20, 9), + // (19,9): warning CS8602: Dereference of a possibly null reference. + // t.Item1.FB.ToString(); // 3 + Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "t.Item1.FB").WithLocation(19, 9), // (22,13): error CS0266: Cannot implicitly convert type '(B, (A, A))' to '(A, (B, B))'. An explicit conversion exists (are you missing a cast?) // u = t; // 4 Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "t").WithArguments("(B, (A, A))", "(A, (B, B))").WithLocation(22, 13), @@ -96598,12 +96595,6 @@ static void F3() // (17,9): warning CS8602: Dereference of a possibly null reference. // t1.Item1.F.ToString(); // 3 Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "t1.Item1.F").WithLocation(17, 9), - // (23,9): warning CS8629: Nullable value type may be null. - // t2.Item1.Value.F.ToString(); // 4, 5 - Diagnostic(ErrorCode.WRN_NullableValueTypeMayBeNull, "t2.Item1").WithLocation(23, 9), - // (23,9): warning CS8602: Dereference of a possibly null reference. - // t2.Item1.Value.F.ToString(); // 4, 5 - Diagnostic(ErrorCode.WRN_NullReferenceReceiver, "t2.Item1.Value.F").WithLocation(23, 9), // (28,18): warning CS8619: Nullability of reference types in value of type '(S?, B?)' doesn't match target type '(S?, B)'. // var t3 = ((S?, B))(new S() { F = 3 }, new A()); // 6, 7 Diagnostic(ErrorCode.WRN_NullabilityMismatchInAssignment, "((S?, B))(new S() { F = 3 }, new A())").WithArguments("(S?, B?)", "(S?, B)").WithLocation(28, 18), @@ -97972,7 +97963,7 @@ void symbolValidator(ModuleSymbol m) comp.VerifyDiagnostics(expected .Concat(new[] { // error CS8630: Invalid 'NullableContextOptions' value: 'Enable' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1), + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1), }).ToArray() ); @@ -99258,7 +99249,7 @@ void F3(T3 t3) where T3 : struct? comp.VerifyDiagnostics(expected .Concat(new[] { // error CS8630: Invalid 'NullableContextOptions' value: 'Enable' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1) }).ToArray() ); } @@ -100553,7 +100544,7 @@ public static void F1() where T1 : notnull comp.VerifyDiagnostics(expected .Concat(new[] { // error CS8630: Invalid 'NullableContextOptions' value: 'Enable' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1), + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1), }).ToArray() ); @@ -127246,7 +127237,7 @@ static void F(T? x) where T : struct var comp = CreateCompilation(source, parseOptions: TestOptions.Regular7, options: WithNullableEnable()); comp.VerifyDiagnostics( // error CS8630: Invalid 'NullableContextOptions' value: 'Enable' for C# 7.0. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.0", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.0", "8.0").WithLocation(1, 1) ); } @@ -161484,6 +161475,29 @@ class C }); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82464")] + public void UnreachableWhenClause_SwitchStatement_SingleArm() + { + CreateCompilation(""" + #nullable enable + class C + { + void M(int i, bool b) + { + switch (i) + { + case var x: + case 1 when !b: + break; + } + } + } + """).VerifyDiagnostics( + // (9,18): error CS8120: The switch case is unreachable. It has already been handled by a previous case or it is impossible to match. + // case 1 when !b: + Diagnostic(ErrorCode.ERR_SwitchCaseSubsumed, "1").WithLocation(9, 18)); + } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/66960")] public void Repro66960() { diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/ObjectAndCollectionInitializerTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/ObjectAndCollectionInitializerTests.cs index e72202e57238..63f3ffe9b2d3 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/ObjectAndCollectionInitializerTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/ObjectAndCollectionInitializerTests.cs @@ -1893,6 +1893,36 @@ public static void Main() VerifyOperationTreeAndDiagnosticsForTest(source, expectedOperationTree, expectedDiagnostics); } + [Fact] + public void CS0176ERR_ObjectProhibited_ImplicitIndexerArgumentInObjectInitializer() + { + var source = """ + class C + { + public int this[int i] + { + get => 0; + set { } + } + + public static int P => 0; + } + + class Program + { + static void Main() + { + _ = new C { [new C().P] = 1 }; + } + } + """; + + CreateCompilation(source).VerifyDiagnostics( + // CS0176: Member 'C.P' cannot be accessed with an instance reference; qualify it with a type name instead + // _ = new C { [new C().P] = 1 }; + Diagnostic(ErrorCode.ERR_ObjectProhibited, "new C().P").WithArguments("C.P").WithLocation(16, 22)); + } + [CompilerTrait(CompilerFeature.IOperation)] [Fact] public void CS1917ERR_ReadonlyValueTypeInObjectInitializer() diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs index dd98198cc411..d5a2c0e9cec1 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RecordStructTests.cs @@ -4234,9 +4234,6 @@ record struct A; var comp = CreateCompilation(source); comp.MakeTypeMissing(WellKnownType.System_IEquatable_T); comp.VerifyEmitDiagnostics( - // (2,15): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported - // record struct A; - Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.IEquatable`1").WithLocation(2, 15), // (2,15): error CS0518: Predefined type 'System.IEquatable`1' is not defined or imported // record struct A; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "A").WithArguments("System.IEquatable`1").WithLocation(2, 15) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs index 725a1e993872..aa8719d2a2ed 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs @@ -26939,20 +26939,23 @@ .class public A var comp = CreateCompilation(sourceB, references: new[] { refA }); if (version == 11) { - comp.VerifyDiagnostics(); + comp.VerifyEmitDiagnostics(); } else { comp.VerifyDiagnostics( // (3,41): error CS9103: 'A.F1(out int)' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. // static ref int F2(out int i) => ref A.F1(out i); - Diagnostic(ErrorCode.ERR_UnrecognizedRefSafetyRulesAttributeVersion, "A.F1").WithArguments("A.F1(out int)").WithLocation(3, 41)); + Diagnostic(ErrorCode.ERR_UnrecognizedAttributeVersion, "A.F1").WithArguments("A.F1(out int)", "System.Runtime.CompilerServices.RefSafetyRulesAttribute", "11").WithLocation(3, 41)); } var method = comp.GetMember("A.F1"); VerifyParameterSymbol(method.Parameters[0], "out System.Int32 i", RefKind.Out, version == 11 ? ScopedKind.ScopedRef : ScopedKind.None); Assert.Equal(version == 11, method.ContainingModule.UseUpdatedEscapeRules); + + // 'A.F1' not used => no error + CreateCompilation("class C;", references: [refA]).VerifyEmitDiagnostics(); } [WorkItem(64507, "https://github.com/dotnet/roslyn/issues/64507")] @@ -26986,12 +26989,15 @@ class B comp.VerifyDiagnostics( // (3,41): error CS9103: 'A.F1(out int)' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. // static ref int F2(out int i) => ref A.F1(out i); - Diagnostic(ErrorCode.ERR_UnrecognizedRefSafetyRulesAttributeVersion, "A.F1").WithArguments("A.F1(out int)").WithLocation(3, 41)); + Diagnostic(ErrorCode.ERR_UnrecognizedAttributeVersion, "A.F1").WithArguments("A.F1(out int)", "System.Runtime.CompilerServices.RefSafetyRulesAttribute", "11").WithLocation(3, 41)); var method = comp.GetMember("A.F1"); VerifyParameterSymbol(method.Parameters[0], "out System.Int32 i", RefKind.Out, ScopedKind.None); Assert.False(method.ContainingModule.UseUpdatedEscapeRules); + + // 'A.F1' not used => no error + CreateCompilation("class C;", references: [refA]).VerifyEmitDiagnostics(); } [WorkItem(64507, "https://github.com/dotnet/roslyn/issues/64507")] @@ -27025,12 +27031,15 @@ class B comp.VerifyDiagnostics( // (3,41): error CS9103: 'A.F1(out int)' is defined in a module with an unrecognized RefSafetyRulesAttribute version, expecting '11'. // static ref int F2(out int i) => ref A.F1(out i); - Diagnostic(ErrorCode.ERR_UnrecognizedRefSafetyRulesAttributeVersion, "A.F1").WithArguments("A.F1(out int)").WithLocation(3, 41)); + Diagnostic(ErrorCode.ERR_UnrecognizedAttributeVersion, "A.F1").WithArguments("A.F1(out int)", "System.Runtime.CompilerServices.RefSafetyRulesAttribute", "11").WithLocation(3, 41)); var method = comp.GetMember("A.F1"); VerifyParameterSymbol(method.Parameters[0], "out System.Int32 i", RefKind.Out, ScopedKind.None); Assert.False(method.ContainingModule.UseUpdatedEscapeRules); + + // 'A.F1' not used => no error + CreateCompilation("class C;", references: [refA]).VerifyEmitDiagnostics(); } /// diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/TopLevelStatementsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/TopLevelStatementsTests.cs index ed78fc7d1fc7..0ac69ee0359c 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/TopLevelStatementsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/TopLevelStatementsTests.cs @@ -7621,6 +7621,9 @@ public void MissingTypes_04() // (2,1): error CS0518: Predefined type 'System.Int32' is not defined or imported // await System.Threading.Tasks.Task.Factory.StartNew(() => 5L); Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "await System.Threading.Tasks.Task.Factory.StartNew(() => 5L);").WithArguments("System.Int32").WithLocation(2, 1), + // (2,1): error CS0518: Predefined type 'System.Int32' is not defined or imported + // await System.Threading.Tasks.Task.Factory.StartNew(() => 5L); + Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "await System.Threading.Tasks.Task.Factory.StartNew(() => 5L);").WithArguments("System.Int32").WithLocation(2, 1), // (3,8): error CS0518: Predefined type 'System.Int32' is not defined or imported // return 11; Diagnostic(ErrorCode.ERR_PredefinedTypeNotFound, "11").WithArguments("System.Int32").WithLocation(3, 8) diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/UnsafeTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/UnsafeTests.cs index 38367578cf75..e50ea8e8394b 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/UnsafeTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/UnsafeTests.cs @@ -21,6 +21,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests /// /// Tests related to binding (but not lowering) lock statements. /// + [CompilerTrait(CompilerFeature.Unsafe)] public class UnsafeTests : CompilingTestBase { private static string GetEscapedNewLine() @@ -11407,6 +11408,9 @@ class C : A CreateCompilation(text, parseOptions: TestOptions.Regular11).VerifyDiagnostics(expected); CreateCompilation(text, options: TestOptions.UnsafeReleaseDll, parseOptions: TestOptions.Regular11).VerifyDiagnostics(expected); + CreateCompilation(text, options: TestOptions.ReleaseDll.WithUpdatedMemorySafetyRules()).VerifyDiagnostics(expected); + CreateCompilation(text, options: TestOptions.UnsafeReleaseDll.WithUpdatedMemorySafetyRules()).VerifyDiagnostics(expected); + expected = new[] { // (8,22): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context @@ -13251,6 +13255,54 @@ unsafe void M2(X t) { } Diagnostic(ErrorCode.ERR_UnsafeNeeded, "X").WithLocation(6, 13)); } + [Fact] + public void UsingAlias_Multiple() + { + CreateCompilation(""" + #pragma warning disable CS8019 // unnecessary using + using unsafe X = System.Collections.Generic.List; + using Y = System.Collections.Generic.List; + """, + options: TestOptions.UnsafeReleaseDll).VerifyDiagnostics( + // (3,43): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // using Y = System.Collections.Generic.List; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "long*").WithLocation(3, 43)); + + CreateCompilation(""" + #pragma warning disable CS8019 // unnecessary using + using X = System.Collections.Generic.List; + using unsafe Y = System.Collections.Generic.List; + """, + options: TestOptions.UnsafeReleaseDll).VerifyDiagnostics( + // (2,43): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // using X = System.Collections.Generic.List; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(2, 43)); + } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82426")] + public void UsingStatic_Multiple() + { + CreateCompilation(""" + #pragma warning disable CS8019 // unnecessary using + using static unsafe System.Collections.Generic.List; + using static System.Collections.Generic.List; + """, + options: TestOptions.UnsafeReleaseDll).VerifyDiagnostics( + // (3,46): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // using static System.Collections.Generic.List; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "long*").WithLocation(3, 46)); + + CreateCompilation(""" + #pragma warning disable CS8019 // unnecessary using + using static System.Collections.Generic.List; + using static unsafe System.Collections.Generic.List; + """, + options: TestOptions.UnsafeReleaseDll).VerifyDiagnostics( + // (2,46): error CS0214: Pointers and fixed size buffers may only be used in an unsafe context + // using static System.Collections.Generic.List; + Diagnostic(ErrorCode.ERR_UnsafeNeeded, "int*").WithLocation(2, 46)); + } + [Fact] public void TestStructWithReferenceToItselfThroughAliasPointer1() { diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/UserDefinedConversionTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/UserDefinedConversionTests.cs index 8a3f3cf311f8..c2d4750dc3fb 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/UserDefinedConversionTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/UserDefinedConversionTests.cs @@ -2290,5 +2290,35 @@ public class Class1 { ""subkey"", ""subvalue"" }, }").WithArguments("new(System.StringComparer)").WithLocation(35, 34)); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/81984")] + public void TestAmbiguousUserDefinedConversion_DuplicateErrorMessage() + { + var source = """ + public struct S1 + { + public static implicit operator S1(S2 x) => throw null; + } + + public struct S2 + { + public static implicit operator S1(S2 x) => throw null; + } + + class Program + { + static S1 Test(S2 x) + { + return x; + } + } + """; + + CreateCompilation(source).VerifyDiagnostics( + // (15,16): error CS0457: Ambiguous user defined conversions 'S2.implicit operator S1(S2)' and 'S1.implicit operator S1(S2)' when converting from 'S2' to 'S1' + // return x; + Diagnostic(ErrorCode.ERR_AmbigUDConv, "x").WithArguments("S2.implicit operator S1(S2)", "S1.implicit operator S1(S2)", "S2", "S1").WithLocation(15, 16) + ); + } } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/Utf8StringsLiteralsTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/Utf8StringsLiteralsTests.cs index 6a478e8fe103..fb4d9cdceb60 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/Utf8StringsLiteralsTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/Utf8StringsLiteralsTests.cs @@ -147,12 +147,6 @@ static void Main() // (11,34): error CS0266: Cannot implicitly convert type 'object' to 'System.Span'. An explicit conversion exists (are you missing a cast?) // static Span Test2() => nullConstant; Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "nullConstant").WithArguments("object", "System.Span").WithLocation(11, 34), - // (11,34): error CS0266: Cannot implicitly convert type 'object' to 'System.Span'. An explicit conversion exists (are you missing a cast?) - // static Span Test2() => nullConstant; - Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "nullConstant").WithArguments("object", "System.Span").WithLocation(11, 34), - // (12,42): error CS0266: Cannot implicitly convert type 'object' to 'System.ReadOnlySpan'. An explicit conversion exists (are you missing a cast?) - // static ReadOnlySpan Test3() => nullConstant; - Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "nullConstant").WithArguments("object", "System.ReadOnlySpan").WithLocation(12, 42), // (12,42): error CS0266: Cannot implicitly convert type 'object' to 'System.ReadOnlySpan'. An explicit conversion exists (are you missing a cast?) // static ReadOnlySpan Test3() => nullConstant; Diagnostic(ErrorCode.ERR_NoImplicitConvCast, "nullConstant").WithArguments("object", "System.ReadOnlySpan").WithLocation(12, 42) diff --git a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs index 4da9b403db16..0d155ae49209 100644 --- a/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/SourceGeneration/GeneratorDriverTests.cs @@ -20,11 +20,15 @@ using Roslyn.Test.Utilities.TestGenerators; using Roslyn.Utilities; using Xunit; +using Xunit.Abstractions; + namespace Microsoft.CodeAnalysis.CSharp.Semantic.UnitTests.SourceGeneration { - public class GeneratorDriverTests + public class GeneratorDriverTests(ITestOutputHelper output) : CSharpTestBase { + private readonly ITestOutputHelper _output = output; + [Fact] public void Running_With_No_Changes_Is_NoOp() { @@ -3891,6 +3895,48 @@ public void Diagnostic_SpanOutsideRange_Incremental() compilation.VerifyDiagnostics(); } + [Fact] + [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] + [WorkItem("https://github.com/dotnet/roslyn/issues/82032")] + public void Diagnostic_SpanOutsideRange_Incremental_Update() + { + var source = "class SomewhatLongClassName {}"; + var parseOptions = TestOptions.RegularPreview; + Compilation compilation = CreateCompilation(source, options: TestOptions.DebugDll, parseOptions: parseOptions, sourceFileName: "/original"); + compilation.VerifyDiagnostics(); + + var generator = new PipelineCallbackGenerator(ctx => + { + var inputs = ctx.SyntaxProvider.CreateSyntaxProvider( + static (node, ct) => node.IsKind(SyntaxKind.ClassDeclaration), + static (ctx, ct) => 0); + ctx.RegisterSourceOutput(inputs, (ctx, input) => + { + var tree = compilation.SyntaxTrees.Single(); + ctx.ReportDiagnostic(CodeAnalysis.Diagnostic.Create( + "TEST0001", + "Test", + "Test diagnostic", + DiagnosticSeverity.Warning, + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + warningLevel: 1, + location: Location.Create(tree, TextSpan.FromBounds(20, 21)))); + }); + }).AsSourceGenerator(); + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new[] { generator }, parseOptions: parseOptions); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); + diagnostics.Verify(Diagnostic("TEST0001", "a").WithLocation(1, 21)); + + source = "class C {}"; + compilation = compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.Single(), CSharpSyntaxTree.ParseText(source, parseOptions, path: "/original")); + driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out diagnostics); + + VerifyArgumentExceptionDiagnostic(diagnostics.Single(), nameof(PipelineCallbackGenerator), string.Format(CodeAnalysisResources.InvalidDiagnosticLocationReported, "TEST0001", "/original"), "diagnostic"); + compilation.VerifyDiagnostics(); + } + [ConditionalFact(typeof(IsEnglishLocal))] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1805836")] public void Diagnostic_SpanOutsideRange_Incremental_AdditionalLocations() @@ -4237,19 +4283,16 @@ static void replace(ref Compilation compilation, CSharpParseOptions parseOptions } } - private static void VerifyArgumentExceptionDiagnostic( + private void VerifyArgumentExceptionDiagnostic( Diagnostic diagnostic, string generatorName, string message, string parameterName, bool initialization = false) { - var expectedMessage = -#if NET - $"{message} (Parameter '{parameterName}')"; -#else - $"{message}{Environment.NewLine}Parameter name: {parameterName}"; -#endif + _output.WriteLine(diagnostic.ToString()); + + var expectedMessage = new ArgumentException(message: message, paramName: parameterName).Message; VerifyGeneratorExceptionDiagnostic(diagnostic, generatorName, expectedMessage, initialization); } diff --git a/src/Compilers/CSharp/Test/Semantic/Utilities/ValueSetTests.cs b/src/Compilers/CSharp/Test/Semantic/Utilities/ValueSetTests.cs index 38d62907f314..433eaa3e950f 100644 --- a/src/Compilers/CSharp/Test/Semantic/Utilities/ValueSetTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Utilities/ValueSetTests.cs @@ -33,7 +33,7 @@ public class ValueSetTests [InlineData(int.MaxValue)] public void TestGE_01(int i1) { - IValueSet values = ForInt.Related(GreaterThanOrEqual, i1); + IConstantValueSet values = ForInt.Related(GreaterThanOrEqual, i1); Assert.Equal($"[{i1}..{int.MaxValue}]", values.ToString()); } @@ -43,7 +43,7 @@ public void TestGE_02() for (int i = 0; i < 100; i++) { int i1 = Random.Next(int.MinValue, int.MaxValue); - IValueSet values = ForInt.Related(GreaterThanOrEqual, i1); + IConstantValueSet values = ForInt.Related(GreaterThanOrEqual, i1); Assert.Equal($"[{i1}..{int.MaxValue}]", values.ToString()); } } @@ -62,7 +62,7 @@ public void TestGE_02() [InlineData(int.MaxValue)] public void TestGT_01(int i1) { - IValueSet values = ForInt.Related(GreaterThan, i1); + IConstantValueSet values = ForInt.Related(GreaterThan, i1); Assert.Equal((i1 == int.MaxValue) ? "" : $"[{i1 + 1}..{int.MaxValue}]", values.ToString()); } @@ -72,7 +72,7 @@ public void TestGT_02() for (int i = 0; i < 100; i++) { int i1 = Random.Next(int.MinValue, int.MaxValue); - IValueSet values = ForInt.Related(GreaterThan, i1); + IConstantValueSet values = ForInt.Related(GreaterThan, i1); Assert.Equal($"[{i1 + 1}..{int.MaxValue}]", values.ToString()); } } @@ -91,7 +91,7 @@ public void TestGT_02() [InlineData(int.MaxValue)] public void TestLE_01(int i1) { - IValueSet values = ForInt.Related(LessThanOrEqual, i1); + IConstantValueSet values = ForInt.Related(LessThanOrEqual, i1); Assert.Equal($"[{int.MinValue}..{i1}]", values.ToString()); } @@ -101,7 +101,7 @@ public void TestLE_02() for (int i = 0; i < 100; i++) { int i1 = Random.Next(int.MinValue, int.MaxValue) + 1; - IValueSet values = ForInt.Related(LessThanOrEqual, i1); + IConstantValueSet values = ForInt.Related(LessThanOrEqual, i1); Assert.Equal($"[{int.MinValue}..{i1}]", values.ToString()); } } @@ -120,7 +120,7 @@ public void TestLE_02() [InlineData(int.MaxValue)] public void TestLT_01(int i1) { - IValueSet values = ForInt.Related(LessThan, i1); + IConstantValueSet values = ForInt.Related(LessThan, i1); Assert.Equal((i1 == int.MinValue) ? "" : $"[{int.MinValue}..{i1 - 1}]", values.ToString()); } @@ -130,7 +130,7 @@ public void TestLT_02() for (int i = 0; i < 100; i++) { int i1 = Random.Next(int.MinValue, int.MaxValue) + 1; - IValueSet values = ForInt.Related(LessThan, i1); + IConstantValueSet values = ForInt.Related(LessThan, i1); Assert.Equal($"[{int.MinValue}..{i1 - 1}]", values.ToString()); } } @@ -149,7 +149,7 @@ public void TestLT_02() [InlineData(int.MaxValue)] public void TestEQ_01(int i1) { - IValueSet values = ForInt.Related(Equal, i1); + IConstantValueSet values = ForInt.Related(Equal, i1); Assert.Equal($"[{i1}..{i1}]", values.ToString()); } @@ -159,7 +159,7 @@ public void TestEQ_02() for (int i = 0; i < 100; i++) { int i1 = Random.Next(int.MinValue, int.MaxValue); - IValueSet values = ForInt.Related(Equal, i1); + IConstantValueSet values = ForInt.Related(Equal, i1); Assert.Equal($"[{i1}..{i1}]", values.ToString()); } } @@ -172,9 +172,9 @@ public void TestIntersect_01() int i1 = Random.Next(int.MinValue + 1, int.MaxValue); int i2 = Random.Next(int.MinValue, int.MaxValue); if (i1 > i2) (i1, i2) = (i2, i1); - IValueSet values1 = ForInt.Related(GreaterThanOrEqual, i1).Intersect(ForInt.Related(LessThanOrEqual, i2)); + IConstantValueSet values1 = ForInt.Related(GreaterThanOrEqual, i1).Intersect(ForInt.Related(LessThanOrEqual, i2)); Assert.Equal($"[{i1}..{i2}]", values1.ToString()); - IValueSet values2 = ForInt.Related(LessThanOrEqual, i2).Intersect(ForInt.Related(GreaterThanOrEqual, i1)); + IConstantValueSet values2 = ForInt.Related(LessThanOrEqual, i2).Intersect(ForInt.Related(GreaterThanOrEqual, i1)); Assert.Equal(values1, values2); } } @@ -188,9 +188,9 @@ public void TestIntersect_02() int i2 = Random.Next(int.MinValue, int.MaxValue); if (i1 < i2) (i1, i2) = (i2, i1); if (i1 == i2) continue; - IValueSet values1 = ForInt.Related(GreaterThanOrEqual, i1).Intersect(ForInt.Related(LessThanOrEqual, i2)); + IConstantValueSet values1 = ForInt.Related(GreaterThanOrEqual, i1).Intersect(ForInt.Related(LessThanOrEqual, i2)); Assert.Equal($"", values1.ToString()); - IValueSet values2 = ForInt.Related(LessThanOrEqual, i2).Intersect(ForInt.Related(GreaterThanOrEqual, i1)); + IConstantValueSet values2 = ForInt.Related(LessThanOrEqual, i2).Intersect(ForInt.Related(GreaterThanOrEqual, i1)); Assert.Equal(values1, values2); } } @@ -204,9 +204,9 @@ public void TestUnion_01() int i2 = Random.Next(int.MinValue, int.MaxValue); if (i1 > i2) (i1, i2) = (i2, i1); if ((i1 + 1) >= i2) continue; - IValueSet values1 = ForInt.Related(LessThanOrEqual, i1).Union(ForInt.Related(GreaterThanOrEqual, i2)); + IConstantValueSet values1 = ForInt.Related(LessThanOrEqual, i1).Union(ForInt.Related(GreaterThanOrEqual, i2)); Assert.Equal($"[{int.MinValue}..{i1}],[{i2}..{int.MaxValue}]", values1.ToString()); - IValueSet values2 = ForInt.Related(GreaterThanOrEqual, i2).Union(ForInt.Related(LessThanOrEqual, i1)); + IConstantValueSet values2 = ForInt.Related(GreaterThanOrEqual, i2).Union(ForInt.Related(LessThanOrEqual, i1)); Assert.Equal(values1, values2); } } @@ -219,9 +219,9 @@ public void TestUnion_02() int i1 = Random.Next(int.MinValue + 1, int.MaxValue); int i2 = Random.Next(int.MinValue, int.MaxValue); if (i1 < i2) (i1, i2) = (i2, i1); - IValueSet values1 = ForInt.Related(LessThanOrEqual, i1).Union(ForInt.Related(GreaterThanOrEqual, i2)); + IConstantValueSet values1 = ForInt.Related(LessThanOrEqual, i1).Union(ForInt.Related(GreaterThanOrEqual, i2)); Assert.Equal($"[{int.MinValue}..{int.MaxValue}]", values1.ToString()); - IValueSet values2 = ForInt.Related(GreaterThanOrEqual, i2).Union(ForInt.Related(LessThanOrEqual, i1)); + IConstantValueSet values2 = ForInt.Related(GreaterThanOrEqual, i2).Union(ForInt.Related(LessThanOrEqual, i1)); Assert.Equal(values1, values2); } } @@ -235,9 +235,9 @@ public void TestComplement_01() int i2 = Random.Next(int.MinValue, int.MaxValue); if (i1 > i2) (i1, i2) = (i2, i1); if ((i1 + 1) >= i2) continue; - IValueSet values1 = ForInt.Related(LessThanOrEqual, i1).Union(ForInt.Related(GreaterThanOrEqual, i2)); + IConstantValueSet values1 = ForInt.Related(LessThanOrEqual, i1).Union(ForInt.Related(GreaterThanOrEqual, i2)); Assert.Equal($"[{int.MinValue}..{i1}],[{i2}..{int.MaxValue}]", values1.ToString()); - IValueSet values2 = values1.Complement(); + IConstantValueSet values2 = values1.Complement(); Assert.Equal(values1, values2.Complement()); Assert.Equal($"[{i1 + 1}..{i2 - 1}]", values2.ToString()); } @@ -251,7 +251,7 @@ public void TestAny_01() int i1 = Random.Next(int.MinValue, int.MaxValue); int i2 = Random.Next(int.MinValue, int.MaxValue); if (i1 > i2) (i1, i2) = (i2, i1); - IValueSet values = ForInt.Related(GreaterThanOrEqual, i1).Intersect(ForInt.Related(LessThanOrEqual, i2)); + IConstantValueSet values = ForInt.Related(GreaterThanOrEqual, i1).Intersect(ForInt.Related(LessThanOrEqual, i2)); Assert.Equal($"[{i1}..{i2}]", values.ToString()); test(int.MinValue); if (i1 != int.MinValue) test(i1 - 1); @@ -279,7 +279,7 @@ public void TestIsEmpty_01() { int i1 = Random.Next(int.MinValue, int.MaxValue); int i2 = Random.Next(int.MinValue, int.MaxValue); - IValueSet values = ForInt.Related(GreaterThanOrEqual, i1).Intersect(ForInt.Related(LessThanOrEqual, i2)); + IConstantValueSet values = ForInt.Related(GreaterThanOrEqual, i1).Intersect(ForInt.Related(LessThanOrEqual, i2)); Assert.Equal(values.ToString().Length == 0, values.IsEmpty); } } @@ -292,7 +292,7 @@ public void TestDouble_01() double d1 = Random.NextDouble() * 100 - 50; double d2 = Random.NextDouble() * 100 - 50; if (d1 > d2) (d1, d2) = (d2, d1); - IValueSet values = ForDouble.Related(GreaterThanOrEqual, d1).Intersect(ForDouble.Related(LessThanOrEqual, d2)); + IConstantValueSet values = ForDouble.Related(GreaterThanOrEqual, d1).Intersect(ForDouble.Related(LessThanOrEqual, d2)); Assert.Equal(FormattableString.Invariant($"[{d1:G17}..{d2:G17}]"), values.ToString()); } } @@ -300,10 +300,10 @@ public void TestDouble_01() [Fact] public void TestChar_01() { - IValueSet gea1 = ForChar.Related(GreaterThanOrEqual, 'a'); - IValueSet lez1 = ForChar.Related(LessThanOrEqual, 'z'); - IValueSet gea2 = ForChar.Related(GreaterThanOrEqual, 'A'); - IValueSet lez2 = ForChar.Related(LessThanOrEqual, 'Z'); + IConstantValueSet gea1 = ForChar.Related(GreaterThanOrEqual, 'a'); + IConstantValueSet lez1 = ForChar.Related(LessThanOrEqual, 'z'); + IConstantValueSet gea2 = ForChar.Related(GreaterThanOrEqual, 'A'); + IConstantValueSet lez2 = ForChar.Related(LessThanOrEqual, 'Z'); var letters = gea1.Intersect(lez1).Union(gea2.Intersect(lez2)); Assert.Equal("['A'..'Z'],['a'..'z']", letters.ToString()); } @@ -591,7 +591,7 @@ public void TestNumbers_Fuzz_01() { var Random = new Random(123445); - foreach (var fac in new IValueSetFactory[] { + foreach (var fac in new IConstantValueSetFactory[] { ForByte, ForSByte, ForShort, ForUShort, ForInt, ForUInt, ForLong, ForULong, ForFloat, ForDouble, ForDecimal, ForNint, @@ -615,7 +615,7 @@ public void TestNumbers_Fuzz_01() [Fact] public void TestNumbers_Fuzz_02() { - foreach (var fac in new IValueSetFactory[] { + foreach (var fac in new IConstantValueSetFactory[] { ForByte, ForSByte, ForShort, ForUShort, ForInt, ForUInt, ForLong, ForULong, ForDecimal, ForNint, @@ -675,7 +675,7 @@ public void TestString_Fuzz_02() Assert.Equal(i1, i3); Assert.Equal(i1, i4); - s1 = s1.Complement(); + s1 = (IConstantValueSet)s1.Complement(); u1 = s1.Union(s2); u2 = s1.Complement().Intersect(s2.Complement()).Complement(); @@ -693,7 +693,7 @@ public void TestString_Fuzz_02() Assert.Equal(i1, i3); Assert.Equal(i1, i4); - s2 = s2.Complement(); + s2 = (IConstantValueSet)s2.Complement(); u1 = s1.Union(s2); u2 = s1.Complement().Intersect(s2.Complement()).Complement(); @@ -852,7 +852,7 @@ public void TestAllFuzz_04() public void DoNotCrashOnBadInput() { // For error recovery, do not throw exceptions on bad inputs. - var ctors = new IValueSetFactory[] + var ctors = new IConstantValueSetFactory[] { ForByte, ForSByte, @@ -873,11 +873,11 @@ public void DoNotCrashOnBadInput() ForLength, }; ConstantValue badConstant = ConstantValue.Bad; - foreach (IValueSetFactory fac in ctors) + foreach (IConstantValueSetFactory fac in ctors) { foreach (BinaryOperatorKind relation in new[] { LessThan, Equal, NotEqual }) { - IValueSet set = fac.Related(relation, badConstant); + IConstantValueSet set = fac.Related(relation, badConstant); _ = set.All(relation, badConstant); _ = set.Any(relation, badConstant); } diff --git a/src/Compilers/CSharp/Test/Symbol/Compilation/CSharpCompilationOptionsTests.cs b/src/Compilers/CSharp/Test/Symbol/Compilation/CSharpCompilationOptionsTests.cs index 13a70fbfb5f7..32880bcadede 100644 --- a/src/Compilers/CSharp/Test/Symbol/Compilation/CSharpCompilationOptionsTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Compilation/CSharpCompilationOptionsTests.cs @@ -366,7 +366,9 @@ public void TestFieldsForEqualsAndGetHashCode() typeof(CSharpCompilationOptions), "Language", "AllowUnsafe", + "MemorySafetyRules", "Usings", + "UseUpdatedMemorySafetyRules", "TopLevelBinderFlags", "NullableContextOptions"); } diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/ExtensionMethodTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/ExtensionMethodTests.cs index 011ecb35c772..4e905913e099 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/ExtensionMethodTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/ExtensionMethodTests.cs @@ -2454,19 +2454,19 @@ internal static void M(this object o) { } var mscorlib = type.GetMember("F").Type.ContainingAssembly; Assert.Equal(RuntimeCorLibName.Name, mscorlib.Name); // We assume every PE assembly may contain extension methods. - Assert.True(mscorlib.MightContainExtensionMethods); + Assert.True(mscorlib.MightContainExtensions); // TODO: Original references are not included in symbol validator. if (isFromSource) { // System.Core.dll var systemCore = type.GetMember("G").Type.ContainingAssembly; - Assert.True(systemCore.MightContainExtensionMethods); + Assert.True(systemCore.MightContainExtensions); } // Local assembly. var assembly = type.ContainingAssembly; - Assert.True(assembly.MightContainExtensionMethods); + Assert.True(assembly.MightContainExtensions); }; CompileAndVerify( @@ -2493,7 +2493,7 @@ internal static void M(object o) { } Func> validator = isFromSource => module => { var assembly = module.ContainingAssembly; - var mightContainExtensionMethods = assembly.MightContainExtensionMethods; + var mightContainExtensionMethods = assembly.MightContainExtensions; // Every PE assembly is assumed to be capable of having an extension method. // The source assembly doesn't know (so reports "true") until all methods have been inspected. Assert.True(mightContainExtensionMethods); @@ -2505,7 +2505,7 @@ internal static void M(object o) { } }; CompileAndVerify(source, symbolValidator: validator(false), sourceSymbolValidator: validator(true)); Assert.NotNull(sourceAssembly); - Assert.False(sourceAssembly.MightContainExtensionMethods); + Assert.False(sourceAssembly.MightContainExtensions); } [ClrOnlyFact] @@ -4485,5 +4485,20 @@ .maxstack 8 // ptr.M(); Diagnostic(ErrorCode.ERR_NoSuchMemberOrExtension, "M").WithArguments("delegate*", "M").WithLocation(4, 9)); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82206")] + public void MightContainExtensionMethods_01() + { + // For source assembly symbol, the initial value is always true. + var comp = CreateCompilation(""); + Assert.True(comp.SourceAssembly.MightContainExtensions); + Assert.True(comp.SourceAssembly.GetPublicSymbol().MightContainExtensionMethods); + comp.VerifyEmitDiagnostics(); + + // But once the actual value is determined, it may be updated to false. + // This may be surprising from a public API perspective. + Assert.False(comp.SourceAssembly.MightContainExtensions); + Assert.False(comp.SourceAssembly.GetPublicSymbol().MightContainExtensionMethods); + } } } diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/MissingSpecialMember.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/MissingSpecialMember.cs index 65d98b42c19b..9ee4848bfa7b 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/MissingSpecialMember.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/MissingSpecialMember.cs @@ -620,6 +620,8 @@ public void AllWellKnownTypes() case WellKnownType.System_Runtime_CompilerServices_IsByRefLikeAttribute: case WellKnownType.System_Span_T: case WellKnownType.System_ReadOnlySpan_T: + case WellKnownType.System_Memory_T: + case WellKnownType.System_ReadOnlyMemory_T: case WellKnownType.System_Collections_Immutable_ImmutableArray_T: case WellKnownType.System_Runtime_CompilerServices_IsUnmanagedAttribute: case WellKnownType.System_Index: @@ -645,6 +647,8 @@ public void AllWellKnownTypes() case WellKnownType.System_Diagnostics_CodeAnalysis_SetsRequiredMembersAttribute: case WellKnownType.System_Runtime_CompilerServices_ScopedRefAttribute: case WellKnownType.System_Runtime_CompilerServices_RefSafetyRulesAttribute: + case WellKnownType.System_Runtime_CompilerServices_MemorySafetyRulesAttribute: + case WellKnownType.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute: case WellKnownType.System_MemoryExtensions: case WellKnownType.System_Runtime_CompilerServices_CompilerFeatureRequiredAttribute: case WellKnownType.System_Diagnostics_CodeAnalysis_UnscopedRefAttribute: @@ -653,6 +657,8 @@ public void AllWellKnownTypes() case WellKnownType.System_Runtime_CompilerServices_Unsafe: case WellKnownType.System_Runtime_CompilerServices_ParamCollectionAttribute: case WellKnownType.System_Runtime_CompilerServices_ExtensionMarkerAttribute: + case WellKnownType.System_Runtime_CompilerServices_UnionAttribute: + case WellKnownType.System_Runtime_CompilerServices_IUnion: case WellKnownType.System_Runtime_CompilerServices_InlineArray2: case WellKnownType.System_Runtime_CompilerServices_InlineArray3: case WellKnownType.System_Runtime_CompilerServices_InlineArray4: @@ -980,12 +986,14 @@ public void AllWellKnownTypeMembers() case WellKnownMember.System_Span_T__ctor_Array: case WellKnownMember.System_Span_T__get_Item: case WellKnownMember.System_Span_T__get_Length: + case WellKnownMember.System_Span_T__Slice_Int: case WellKnownMember.System_Span_T__Slice_Int_Int: case WellKnownMember.System_ReadOnlySpan_T__ctor_Pointer: case WellKnownMember.System_ReadOnlySpan_T__ctor_Array: case WellKnownMember.System_ReadOnlySpan_T__ctor_Array_Start_Length: case WellKnownMember.System_ReadOnlySpan_T__get_Item: case WellKnownMember.System_ReadOnlySpan_T__get_Length: + case WellKnownMember.System_ReadOnlySpan_T__Slice_Int: case WellKnownMember.System_ReadOnlySpan_T__Slice_Int_Int: case WellKnownMember.System_Index__ctor: case WellKnownMember.System_Index__GetOffset: @@ -1032,6 +1040,8 @@ public void AllWellKnownTypeMembers() case WellKnownMember.System_Diagnostics_CodeAnalysis_SetsRequiredMembersAttribute__ctor: case WellKnownMember.System_Runtime_CompilerServices_ScopedRefAttribute__ctor: case WellKnownMember.System_Runtime_CompilerServices_RefSafetyRulesAttribute__ctor: + case WellKnownMember.System_Runtime_CompilerServices_MemorySafetyRulesAttribute__ctor: + case WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor: case WellKnownMember.System_MemoryExtensions__SequenceEqual_Span_T: case WellKnownMember.System_MemoryExtensions__SequenceEqual_ReadOnlySpan_T: case WellKnownMember.System_MemoryExtensions__AsSpan_String: @@ -1047,6 +1057,10 @@ public void AllWellKnownTypeMembers() case WellKnownMember.System_Runtime_CompilerServices_RequiresLocationAttribute__ctor: case WellKnownMember.System_Runtime_CompilerServices_ParamCollectionAttribute__ctor: case WellKnownMember.System_Runtime_CompilerServices_ExtensionMarkerAttribute__ctor: + case WellKnownMember.System_Memory_T__Slice_Int: + case WellKnownMember.System_Memory_T__Slice_Int_Int: + case WellKnownMember.System_ReadOnlyMemory_T__Slice_Int: + case WellKnownMember.System_ReadOnlyMemory_T__Slice_Int_Int: case WellKnownMember.System_Runtime_CompilerServices_ClosedAttribute__ctor: // Not yet in the platform. continue; @@ -1088,6 +1102,7 @@ public void AllWellKnownTypeMembers() case WellKnownMember.System_Runtime_CompilerServices_IsReadOnlyAttribute__ctor: case WellKnownMember.System_Runtime_CompilerServices_IsByRefLikeAttribute__ctor: case WellKnownMember.System_Runtime_CompilerServices_IsUnmanagedAttribute__ctor: + case WellKnownMember.System_Runtime_CompilerServices_UnionAttribute__ctor: case WellKnownMember.System_Runtime_CompilerServices_ITuple__get_Item: case WellKnownMember.System_Runtime_CompilerServices_ITuple__get_Length: case WellKnownMember.System_Runtime_InteropServices_CollectionsMarshal__AsSpan_T: diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/MockAssemblySymbol.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/MockAssemblySymbol.cs index 42d03387f3ce..5253f16e1188 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/MockAssemblySymbol.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/MockAssemblySymbol.cs @@ -115,7 +115,7 @@ public override ICollection NamespaceNames } } - public override bool MightContainExtensionMethods + public override bool MightContainExtensions { get { return true; } } diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/MockNamedTypeSymbol.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/MockNamedTypeSymbol.cs index 636d4247ce1e..df6af0ccf3ef 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/MockNamedTypeSymbol.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/MockNamedTypeSymbol.cs @@ -239,7 +239,7 @@ public sealed override bool AreLocalsZeroed } } - public override bool MightContainExtensionMethods + public override bool MightContainExtensions { get { @@ -273,6 +273,8 @@ internal override ImmutableArray GetDeclaredInterfaces(ConsList internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal sealed override ManagedKind GetManagedKind(ref CompoundUseSiteInfo useSiteInfo) => ManagedKind.Managed; internal override bool ShouldAddWinRTMembers diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/Retargeting/RetargetingTests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/Retargeting/RetargetingTests.cs index 7132b52637a1..26c9a2763219 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/Retargeting/RetargetingTests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/Retargeting/RetargetingTests.cs @@ -45,34 +45,37 @@ internal static void E(this T t, U u) { } var retargetingModule = retargetingAssembly.Modules[0]; var retargetingNamespace = retargetingModule.GlobalNamespace; - var sourceMethods = new ArrayBuilder(); - sourceNamespace.GetExtensionMethods(sourceMethods, null, 0, LookupOptions.AllMethodsOnArityZero); + var sourceMethods = new ArrayBuilder(); + sourceNamespace.GetAllExtensionMembers(sourceMethods, name: null, alternativeName: null, arity: 0, LookupOptions.AllMethodsOnArityZero, fieldsBeingBound: null); Utils.CheckSymbols(sourceMethods.ToImmutable(), "void S1.E(object x, object y)", "void S2.E(T t, U u)"); - var retargetingMethods = new ArrayBuilder(); - retargetingNamespace.GetExtensionMethods(retargetingMethods, null, 0, LookupOptions.AllMethodsOnArityZero); + var retargetingMethods = new ArrayBuilder(); + retargetingNamespace.GetAllExtensionMembers(retargetingMethods, name: null, alternativeName: null, arity: 0, LookupOptions.AllMethodsOnArityZero, fieldsBeingBound: null); Utils.CheckSymbols(retargetingMethods.ToImmutable(), "void S1.E(object x, object y)", "void S2.E(T t, U u)"); - for (int i = 0; i < sourceMethods.Count; i++) - { - CheckMethods(sourceMethods[i], retargetingMethods[i]); - } + var s1SourceMethod = sourceMethods.Single(m => m.ContainingType.Name == "S1"); + var s1RetargetingMethod = retargetingMethods.Single(m => m.ContainingType.Name == "S1"); + CheckMethods(s1SourceMethod, s1RetargetingMethod); + + var s2SourceMethod = sourceMethods.Single(m => m.ContainingType.Name == "S2"); + var s2RetargetingMethod = retargetingMethods.Single(m => m.ContainingType.Name == "S2"); + CheckMethods(s2SourceMethod, s2RetargetingMethod); - sourceMethods = new ArrayBuilder(); - sourceNamespace.GetExtensionMethods(sourceMethods, "E", 2, LookupOptions.Default); + sourceMethods = new ArrayBuilder(); + sourceNamespace.GetAllExtensionMembers(sourceMethods, name: "E", alternativeName: null, arity: 2, LookupOptions.Default, fieldsBeingBound: null); Utils.CheckSymbols(sourceMethods.ToImmutable(), "void S2.E(T t, U u)"); - var sourceMethod = sourceMethods[0]; + var sourceMethod = (MethodSymbol)sourceMethods[0]; - retargetingMethods = new ArrayBuilder(); - retargetingNamespace.GetExtensionMethods(retargetingMethods, "E", 2, LookupOptions.Default); + retargetingMethods = new ArrayBuilder(); + retargetingNamespace.GetAllExtensionMembers(retargetingMethods, name: "E", alternativeName: null, arity: 2, LookupOptions.Default, fieldsBeingBound: null); Utils.CheckSymbols(retargetingMethods.ToImmutable(), "void S2.E(T t, U u)"); - var retargetingMethod = retargetingMethods[0]; + var retargetingMethod = (MethodSymbol)retargetingMethods[0]; var sourceType = sourceNamespace.GetMember("C"); var retargetingType = retargetingNamespace.GetMember("C"); diff --git a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/NullablePublicAPITests.cs b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/NullablePublicAPITests.cs index a23ed0923c6e..a94c819f9f9a 100644 --- a/src/Compilers/CSharp/Test/Symbol/Symbols/Source/NullablePublicAPITests.cs +++ b/src/Compilers/CSharp/Test/Symbol/Symbols/Source/NullablePublicAPITests.cs @@ -1817,7 +1817,7 @@ void M(string s) var comp = CreateCompilation(source, options: WithNullableEnable(), parseOptions: TestOptions.Regular7_3, skipUsesIsNullable: true); comp.VerifyDiagnostics( // error CS8630: Invalid 'NullableContextOptions' value: 'Enable' for C# 7.3. Please use language version '8.0' or greater. - Diagnostic(ErrorCode.ERR_NullableOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1) + Diagnostic(ErrorCode.ERR_CompilationOptionNotAvailable).WithArguments("NullableContextOptions", "Enable", "7.3", "8.0").WithLocation(1, 1) ); var analyzer = new CSharp73ProvidesNullableSemanticInfo_Analyzer(); diff --git a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs index f7f815acb509..b21e1edee14f 100644 --- a/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs +++ b/src/Compilers/CSharp/Test/Syntax/Diagnostics/DiagnosticTest.cs @@ -481,6 +481,11 @@ public void WarningLevel_2() // These are the warnings introduced with the warning "wave" shipped with dotnet 10 and C# 14. Assert.Equal(10, ErrorFacts.GetWarningLevel(errorCode)); break; + case ErrorCode.WRN_RequiresUnsafeAttributeLegacyRules: + case ErrorCode.WRN_UnsafeMeaningless: + // These are the warnings introduced with the warning "wave" shipped with dotnet 11 and C# 15. + Assert.Equal(11, ErrorFacts.GetWarningLevel(errorCode)); + break; default: // If a new warning is added, this test will fail // and whoever is adding the new warning will have to update it with the expected error level. diff --git a/src/Compilers/CSharp/Test/Syntax/Generated/Syntax.Test.xml.Generated.cs b/src/Compilers/CSharp/Test/Syntax/Generated/Syntax.Test.xml.Generated.cs index 54fecd24c24f..521d5c7a5921 100644 --- a/src/Compilers/CSharp/Test/Syntax/Generated/Syntax.Test.xml.Generated.cs +++ b/src/Compilers/CSharp/Test/Syntax/Generated/Syntax.Test.xml.Generated.cs @@ -506,7 +506,7 @@ private static Syntax.InternalSyntax.ClassDeclarationSyntax GenerateClassDeclara => InternalSyntaxFactory.ClassDeclaration(new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), InternalSyntaxFactory.Token(SyntaxKind.ClassKeyword), InternalSyntaxFactory.Identifier("Identifier"), null, null, null, new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), null, new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), null, null); private static Syntax.InternalSyntax.StructDeclarationSyntax GenerateStructDeclaration() - => InternalSyntaxFactory.StructDeclaration(new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), InternalSyntaxFactory.Token(SyntaxKind.StructKeyword), InternalSyntaxFactory.Identifier("Identifier"), null, null, null, new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), null, new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), null, null); + => InternalSyntaxFactory.StructDeclaration(SyntaxKind.StructDeclaration, new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), InternalSyntaxFactory.Token(SyntaxKind.StructKeyword), InternalSyntaxFactory.Identifier("Identifier"), null, null, null, new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), null, new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), null, null); private static Syntax.InternalSyntax.InterfaceDeclarationSyntax GenerateInterfaceDeclaration() => InternalSyntaxFactory.InterfaceDeclaration(new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), InternalSyntaxFactory.Token(SyntaxKind.InterfaceKeyword), InternalSyntaxFactory.Identifier("Identifier"), null, null, null, new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), null, new Microsoft.CodeAnalysis.Syntax.InternalSyntax.SyntaxList(), null, null); @@ -10896,7 +10896,7 @@ private static ClassDeclarationSyntax GenerateClassDeclaration() => SyntaxFactory.ClassDeclaration(new SyntaxList(), new SyntaxTokenList(), SyntaxFactory.Token(SyntaxKind.ClassKeyword), SyntaxFactory.Identifier("Identifier"), default(TypeParameterListSyntax), default(ParameterListSyntax), default(BaseListSyntax), new SyntaxList(), default(SyntaxToken), new SyntaxList(), default(SyntaxToken), default(SyntaxToken)); private static StructDeclarationSyntax GenerateStructDeclaration() - => SyntaxFactory.StructDeclaration(new SyntaxList(), new SyntaxTokenList(), SyntaxFactory.Token(SyntaxKind.StructKeyword), SyntaxFactory.Identifier("Identifier"), default(TypeParameterListSyntax), default(ParameterListSyntax), default(BaseListSyntax), new SyntaxList(), default(SyntaxToken), new SyntaxList(), default(SyntaxToken), default(SyntaxToken)); + => SyntaxFactory.StructDeclaration(SyntaxKind.StructDeclaration, new SyntaxList(), new SyntaxTokenList(), SyntaxFactory.Token(SyntaxKind.StructKeyword), SyntaxFactory.Identifier("Identifier"), default(TypeParameterListSyntax), default(ParameterListSyntax), default(BaseListSyntax), new SyntaxList(), default(SyntaxToken), new SyntaxList(), default(SyntaxToken), default(SyntaxToken)); private static InterfaceDeclarationSyntax GenerateInterfaceDeclaration() => SyntaxFactory.InterfaceDeclaration(new SyntaxList(), new SyntaxTokenList(), SyntaxFactory.Token(SyntaxKind.InterfaceKeyword), SyntaxFactory.Identifier("Identifier"), default(TypeParameterListSyntax), default(ParameterListSyntax), default(BaseListSyntax), new SyntaxList(), default(SyntaxToken), new SyntaxList(), default(SyntaxToken), default(SyntaxToken)); diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/ClosedModifierParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/ClosedModifierParsingTests.cs index eb088e0a90ef..4f8379e17fea 100644 --- a/src/Compilers/CSharp/Test/Syntax/Parsing/ClosedModifierParsingTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/ClosedModifierParsingTests.cs @@ -1317,7 +1317,7 @@ public void MemberNamedClosed_05() closed class closed { } """, expectedBindingDiagnostics: [ - // (1,14): error CS9365: Types and aliases cannot be named 'closed'. + // (1,14): error CS9600: Types and aliases cannot be named 'closed'. // closed class closed { } Diagnostic(ErrorCode.ERR_ClosedTypeNameDisallowed, "closed").WithLocation(1, 14) ]); @@ -1867,7 +1867,7 @@ public unsafe class C Diagnostic(ErrorCode.ERR_TypeExpected, "?").WithLocation(8, 18) ], expectedBindingDiagnostics: [ - // (1,15): error CS9365: Types and aliases cannot be named 'closed'. + // (1,15): error CS9600: Types and aliases cannot be named 'closed'. // public struct closed { public int item; } Diagnostic(ErrorCode.ERR_ClosedTypeNameDisallowed, "closed").WithLocation(1, 15), // (3,21): error CS0227: Unsafe code may only appear if compiling with /unsafe diff --git a/src/Compilers/CSharp/Test/Syntax/Parsing/UnionParsingTests.cs b/src/Compilers/CSharp/Test/Syntax/Parsing/UnionParsingTests.cs new file mode 100644 index 000000000000..2dd2376d7d03 --- /dev/null +++ b/src/Compilers/CSharp/Test/Syntax/Parsing/UnionParsingTests.cs @@ -0,0 +1,741 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#nullable disable + +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests; + +public class UnionParsingTests : ParsingTests +{ + public UnionParsingTests(ITestOutputHelper output) : base(output) { } + + [Theory, CombinatorialData] + public void Union_01(bool useCSharp15) + { + var src = """ +union U1(E1); +"""; + UsingTree(src, TestOptions.Regular14, + // (1,12): error CS1001: Identifier expected + // union U1(E1); + Diagnostic(ErrorCode.ERR_IdentifierExpected, ")").WithLocation(1, 12) + ); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.GlobalStatement); + { + N(SyntaxKind.LocalFunctionStatement); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "union"); + } + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + M(SyntaxKind.IdentifierToken); + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.SemicolonToken); + } + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + + UsingTree(src, useCSharp15 ? TestOptions.RegularNext : TestOptions.RegularPreview); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.UnionDeclaration); + { + N(SyntaxKind.UnionKeyword); + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void Union_02() + { + var src = """ +record union U1(E1); +"""; + + UsingTree(src, TestOptions.RegularPreview, + // (1,14): error CS1514: { expected + // record union U1(E1); + Diagnostic(ErrorCode.ERR_LbraceExpected, "U1").WithLocation(1, 14), + // (1,14): error CS1513: } expected + // record union U1(E1); + Diagnostic(ErrorCode.ERR_RbraceExpected, "U1").WithLocation(1, 14), + // (1,14): error CS8803: Top-level statements must precede namespace and type declarations. + // record union U1(E1); + Diagnostic(ErrorCode.ERR_TopLevelStatementAfterNamespaceOrType, "U1(E1);").WithLocation(1, 14) + ); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.RecordDeclaration); + { + N(SyntaxKind.RecordKeyword); + N(SyntaxKind.IdentifierToken, "union"); + M(SyntaxKind.OpenBraceToken); + M(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.GlobalStatement); + { + N(SyntaxKind.ExpressionStatement); + { + N(SyntaxKind.InvocationExpression); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "U1"); + } + N(SyntaxKind.ArgumentList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + } + N(SyntaxKind.CloseParenToken); + } + } + N(SyntaxKind.SemicolonToken); + } + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Theory, CombinatorialData] + public void Union_03(bool useCSharp15) + { + var src = """ +union M() +{ + return default; +} +"""; + UsingTree(src, TestOptions.Regular14); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.GlobalStatement); + { + N(SyntaxKind.LocalFunctionStatement); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "union"); + } + N(SyntaxKind.IdentifierToken, "M"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.ReturnStatement); + { + N(SyntaxKind.ReturnKeyword); + N(SyntaxKind.DefaultLiteralExpression); + { + N(SyntaxKind.DefaultKeyword); + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.CloseBraceToken); + } + } + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + + UsingTree(src, useCSharp15 ? TestOptions.RegularNext : TestOptions.RegularPreview, + // (1,9): error CS1031: Type expected + // union M() + Diagnostic(ErrorCode.ERR_TypeExpected, ")").WithLocation(1, 9), + // (3,5): error CS1519: Invalid token 'return' in a member declaration + // return default; + Diagnostic(ErrorCode.ERR_InvalidMemberDecl, "return").WithArguments("return").WithLocation(3, 5) + ); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.UnionDeclaration); + { + N(SyntaxKind.UnionKeyword); + N(SyntaxKind.IdentifierToken, "M"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + M(SyntaxKind.Parameter); + { + M(SyntaxKind.IdentifierName); + { + M(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Theory, CombinatorialData] + public void Union_04(bool useCSharp15) + { + var src = """ +partial union U1(E1); +"""; + UsingTree(src, TestOptions.Regular14, + // (1,20): error CS1001: Identifier expected + // partial union U1(E1); + Diagnostic(ErrorCode.ERR_IdentifierExpected, ")").WithLocation(1, 20) + ); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PartialKeyword); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "union"); + } + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + M(SyntaxKind.IdentifierToken); + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + + UsingTree(src, useCSharp15 ? TestOptions.RegularNext : TestOptions.RegularPreview); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.UnionDeclaration); + { + N(SyntaxKind.PartialKeyword); + N(SyntaxKind.UnionKeyword); + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Theory, CombinatorialData] + public void Union_05(bool useCSharp15) + { + var src = """ +ref union U1(E1); +"""; + UsingTree(src, TestOptions.Regular14, + // (1,16): error CS1001: Identifier expected + // ref union U1(E1); + Diagnostic(ErrorCode.ERR_IdentifierExpected, ")").WithLocation(1, 16) + ); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.GlobalStatement); + { + N(SyntaxKind.LocalFunctionStatement); + { + N(SyntaxKind.RefType); + { + N(SyntaxKind.RefKeyword); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "union"); + } + } + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + M(SyntaxKind.IdentifierToken); + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.SemicolonToken); + } + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + + UsingTree(src, useCSharp15 ? TestOptions.RegularNext : TestOptions.RegularPreview); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.UnionDeclaration); + { + N(SyntaxKind.RefKeyword); + N(SyntaxKind.UnionKeyword); + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + + var comp = CreateCompilation([src, "struct E1;", UnionAttributeSource, IUnionSource], parseOptions: TestOptions.RegularPreview); + comp.VerifyDiagnostics( + // (1,11): error CS0106: The modifier 'ref' is not valid for this item + // ref union U1(E1); + Diagnostic(ErrorCode.ERR_BadMemberFlag, "U1").WithArguments("ref").WithLocation(1, 11) + ); + + } + + [Theory, CombinatorialData] + public void Union_06(bool useCSharp15) + { + var src = """ +ref partial union U1(E1); +"""; + UsingTree(src, TestOptions.Regular14, + // (1,5): error CS1031: Type expected + // ref partial union U1(E1); + Diagnostic(ErrorCode.ERR_TypeExpected, "partial").WithLocation(1, 5), + // (1,5): error CS1525: Invalid expression term 'partial' + // ref partial union U1(E1); + Diagnostic(ErrorCode.ERR_InvalidExprTerm, "partial").WithArguments("partial").WithLocation(1, 5), + // (1,5): error CS1003: Syntax error, ',' expected + // ref partial union U1(E1); + Diagnostic(ErrorCode.ERR_SyntaxError, "partial").WithArguments(",").WithLocation(1, 5) + ); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.GlobalStatement); + { + N(SyntaxKind.LocalDeclarationStatement); + { + N(SyntaxKind.VariableDeclaration); + { + N(SyntaxKind.RefType); + { + N(SyntaxKind.RefKeyword); + M(SyntaxKind.IdentifierName); + { + M(SyntaxKind.IdentifierToken); + } + } + M(SyntaxKind.VariableDeclarator); + { + M(SyntaxKind.IdentifierToken); + } + } + N(SyntaxKind.SemicolonToken); + } + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + + UsingTree(src, useCSharp15 ? TestOptions.RegularNext : TestOptions.RegularPreview); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.UnionDeclaration); + { + N(SyntaxKind.RefKeyword); + N(SyntaxKind.PartialKeyword); + N(SyntaxKind.UnionKeyword); + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void Union_07() + { + var src = """ +union U1(E1, E2, E3); +"""; + UsingTree(src, TestOptions.RegularPreview); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.UnionDeclaration); + { + N(SyntaxKind.UnionKeyword); + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E2"); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E3"); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void Union_08() + { + var src = """ +union U1(E1) : I1(1); +"""; + UsingTree(src, TestOptions.RegularPreview); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.UnionDeclaration); + { + N(SyntaxKind.UnionKeyword); + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.BaseList); + { + N(SyntaxKind.ColonToken); + N(SyntaxKind.PrimaryConstructorBaseType); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "I1"); + } + N(SyntaxKind.ArgumentList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Argument); + { + N(SyntaxKind.NumericLiteralExpression); + { + N(SyntaxKind.NumericLiteralToken, "1"); + } + } + N(SyntaxKind.CloseParenToken); + } + } + } + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + + var comp = CreateCompilation([src, "struct E1; interface I1;", UnionAttributeSource, IUnionSource], parseOptions: TestOptions.RegularPreview); + comp.VerifyDiagnostics( + // (1,18): error CS8861: Unexpected argument list. + // union U1(E1) : I1(1); + Diagnostic(ErrorCode.ERR_UnexpectedArgumentList, "(1)").WithLocation(1, 18) + ); + } + + [Fact] + public void Union_09() + { + var src = """ +[Attr1] +public union U1(E1) : I1, I2 where T1 : class +{ + public void M1() { } +} +"""; + UsingTree(src, TestOptions.RegularPreview); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.UnionDeclaration); + { + N(SyntaxKind.AttributeList); + { + N(SyntaxKind.OpenBracketToken); + N(SyntaxKind.Attribute); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "Attr1"); + } + } + N(SyntaxKind.CloseBracketToken); + } + N(SyntaxKind.PublicKeyword); + N(SyntaxKind.UnionKeyword); + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.TypeParameterList); + { + N(SyntaxKind.LessThanToken); + N(SyntaxKind.TypeParameter); + { + N(SyntaxKind.IdentifierToken, "T1"); + } + N(SyntaxKind.GreaterThanToken); + } + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.Parameter); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "E1"); + } + } + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.BaseList); + { + N(SyntaxKind.ColonToken); + N(SyntaxKind.SimpleBaseType); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "I1"); + } + } + N(SyntaxKind.CommaToken); + N(SyntaxKind.SimpleBaseType); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "I2"); + } + } + } + N(SyntaxKind.TypeParameterConstraintClause); + { + N(SyntaxKind.WhereKeyword); + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "T1"); + } + N(SyntaxKind.ColonToken); + N(SyntaxKind.ClassConstraint); + { + N(SyntaxKind.ClassKeyword); + } + } + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.MethodDeclaration); + { + N(SyntaxKind.PublicKeyword); + N(SyntaxKind.PredefinedType); + { + N(SyntaxKind.VoidKeyword); + } + N(SyntaxKind.IdentifierToken, "M1"); + N(SyntaxKind.ParameterList); + { + N(SyntaxKind.OpenParenToken); + N(SyntaxKind.CloseParenToken); + } + N(SyntaxKind.Block); + { + N(SyntaxKind.OpenBraceToken); + N(SyntaxKind.CloseBraceToken); + } + } + N(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void Union_10() + { + var src = """ +union U1; +"""; + UsingTree(src, TestOptions.RegularPreview); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.UnionDeclaration); + { + N(SyntaxKind.UnionKeyword); + N(SyntaxKind.IdentifierToken, "U1"); + N(SyntaxKind.SemicolonToken); + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } + + [Fact] + public void Union_11() + { + var src = """ +record union U1; +"""; + UsingTree(src, TestOptions.RegularPreview, + // (1,14): error CS1514: { expected + // record union U1; + Diagnostic(ErrorCode.ERR_LbraceExpected, "U1").WithLocation(1, 14), + // (1,14): error CS1513: } expected + // record union U1; + Diagnostic(ErrorCode.ERR_RbraceExpected, "U1").WithLocation(1, 14), + // (1,14): error CS8803: Top-level statements must precede namespace and type declarations. + // record union U1; + Diagnostic(ErrorCode.ERR_TopLevelStatementAfterNamespaceOrType, "U1;").WithLocation(1, 14) + ); + + N(SyntaxKind.CompilationUnit); + { + N(SyntaxKind.RecordDeclaration); + { + N(SyntaxKind.RecordKeyword); + N(SyntaxKind.IdentifierToken, "union"); + M(SyntaxKind.OpenBraceToken); + M(SyntaxKind.CloseBraceToken); + } + N(SyntaxKind.GlobalStatement); + { + N(SyntaxKind.ExpressionStatement); + { + N(SyntaxKind.IdentifierName); + { + N(SyntaxKind.IdentifierToken, "U1"); + } + N(SyntaxKind.SemicolonToken); + } + } + N(SyntaxKind.EndOfFileToken); + } + EOF(); + } +} diff --git a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNodeCacheTests.cs b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNodeCacheTests.cs index fb3fb7ff9803..158eaa2b3522 100644 --- a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNodeCacheTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNodeCacheTests.cs @@ -18,10 +18,10 @@ public class SyntaxNodeCacheTests : CSharpTestBase public void TryGetNode_With1Child() { var child0 = new SyntaxTokenWithTrivia(SyntaxKind.IntKeyword, null, null); - SyntaxNodeCache.AddNode(child0, child0.GetCacheHash()); + SyntaxNodeCache.AddNode(child0, SyntaxNodeCache.GetCacheHash(child0)); var listOf1 = new PredefinedTypeSyntax(SyntaxKind.PredefinedType, child0); - SyntaxNodeCache.AddNode(listOf1, listOf1.GetCacheHash()); + SyntaxNodeCache.AddNode(listOf1, SyntaxNodeCache.GetCacheHash(listOf1)); var listCached = (PredefinedTypeSyntax)SyntaxNodeCache.TryGetNode(listOf1.RawKind, child0, SyntaxNodeCache.GetDefaultNodeFlags(), out _); Assert.NotNull(listCached); @@ -36,11 +36,11 @@ public void TryGetNode_With1Of2Children() { var child0 = new SyntaxTokenWithTrivia(SyntaxKind.InternalKeyword, null, null); var child1 = new SyntaxTokenWithTrivia(SyntaxKind.StaticKeyword, null, null); - SyntaxNodeCache.AddNode(child0, child0.GetCacheHash()); - SyntaxNodeCache.AddNode(child1, child1.GetCacheHash()); + SyntaxNodeCache.AddNode(child0, SyntaxNodeCache.GetCacheHash(child0)); + SyntaxNodeCache.AddNode(child1, SyntaxNodeCache.GetCacheHash(child1)); var listOf2 = new CodeAnalysis.Syntax.InternalSyntax.SyntaxList.WithTwoChildren(child0, child1); - SyntaxNodeCache.AddNode(listOf2, listOf2.GetCacheHash()); + SyntaxNodeCache.AddNode(listOf2, SyntaxNodeCache.GetCacheHash(listOf2)); var listCached = (CodeAnalysis.Syntax.InternalSyntax.SyntaxList.WithTwoChildren)SyntaxNodeCache.TryGetNode(listOf2.RawKind, child0, child1, SyntaxNodeCache.GetDefaultNodeFlags(), out _); Assert.NotNull(listCached); @@ -60,12 +60,12 @@ public void TryGetNode_With2Of3Children() var child0 = new SyntaxTokenWithTrivia(SyntaxKind.InternalKeyword, null, null); var child1 = new SyntaxTokenWithTrivia(SyntaxKind.StaticKeyword, null, null); var child2 = new SyntaxTokenWithTrivia(SyntaxKind.ReadOnlyKeyword, null, null); - SyntaxNodeCache.AddNode(child0, child0.GetCacheHash()); - SyntaxNodeCache.AddNode(child1, child1.GetCacheHash()); - SyntaxNodeCache.AddNode(child2, child2.GetCacheHash()); + SyntaxNodeCache.AddNode(child0, SyntaxNodeCache.GetCacheHash(child0)); + SyntaxNodeCache.AddNode(child1, SyntaxNodeCache.GetCacheHash(child1)); + SyntaxNodeCache.AddNode(child2, SyntaxNodeCache.GetCacheHash(child2)); var listOf3 = new CodeAnalysis.Syntax.InternalSyntax.SyntaxList.WithThreeChildren(child0, child1, child2); - SyntaxNodeCache.AddNode(listOf3, listOf3.GetCacheHash()); + SyntaxNodeCache.AddNode(listOf3, SyntaxNodeCache.GetCacheHash(listOf3)); var listCached = (CodeAnalysis.Syntax.InternalSyntax.SyntaxList.WithThreeChildren)SyntaxNodeCache.TryGetNode(listOf3.RawKind, child0, child1, child2, SyntaxNodeCache.GetDefaultNodeFlags(), out _); Assert.NotNull(listCached); diff --git a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNormalizerTests.cs b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNormalizerTests.cs index 14afa6fec454..c9f73646240a 100644 --- a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNormalizerTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxNormalizerTests.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using Microsoft.CodeAnalysis.CSharp.Test.Utilities; using Roslyn.Test.Utilities; using Xunit; @@ -3492,7 +3493,7 @@ public void TestNormalizeRegion2() private static void TestNormalizeDeclaration(string text, string expected) { - var node = SyntaxFactory.ParseCompilationUnit(text.NormalizeLineEndings()); + var node = SyntaxFactory.ParseCompilationUnit(text.NormalizeLineEndings(), options: TestOptions.RegularPreview); Assert.Equal(text.NormalizeLineEndings(), node.ToFullString().NormalizeLineEndings()); var actual = node.NormalizeWhitespace(" ").ToFullString(); AssertEx.Equal(expected.NormalizeLineEndings(), actual.NormalizeLineEndings()); @@ -6266,6 +6267,22 @@ public void TestNormalizeSuppression_03() M()!; """, """ M()!; +"""); + } + + [Fact] + public void UnionDeclaration_01() + { + TestNormalizeDeclaration(""" +[Attr1]public union Result < T , E > ( Ok , Err ):IResult where T:class{public void M ( ) { }} +""", """ +[Attr1] +public union Result(Ok, Err) : IResult where T : class +{ + public void M() + { + } +} """); } } diff --git a/src/Compilers/CSharp/csc/AnyCpu/csc.csproj b/src/Compilers/CSharp/csc/AnyCpu/csc.csproj index 5708cc5a9117..4d9585be86bc 100644 --- a/src/Compilers/CSharp/csc/AnyCpu/csc.csproj +++ b/src/Compilers/CSharp/csc/AnyCpu/csc.csproj @@ -5,7 +5,6 @@ Exe $(NetRoslynSourceBuild);net472 true - false diff --git a/src/Compilers/Core/CodeAnalysisTest/InvokeUtil.cs b/src/Compilers/Core/CodeAnalysisTest/InvokeUtil.cs index 408434fc7e3c..e5217cffb8cd 100644 --- a/src/Compilers/Core/CodeAnalysisTest/InvokeUtil.cs +++ b/src/Compilers/Core/CodeAnalysisTest/InvokeUtil.cs @@ -63,6 +63,15 @@ internal void Exec( } var loader = new AnalyzerAssemblyLoader(pathResolvers, assemblyResolvers, compilerLoadContext: null); + + // Ensure that test infrastructure assemblies (e.g. DiffPlex and its transitive + // dependencies such as Microsoft.Win32.Registry) are already loaded before we + // take the snapshot below. AssertEx has static fields that initialize DiffPlex + // on first use; if that first use happens *after* the snapshot is taken then + // those assemblies show up as unexpected additions and the assertion at the end + // of this method fails intermittently (most visibly in the LoadStream variant). + RuntimeHelpers.RunClassConstructor(typeof(AssertEx).TypeHandle); + var compilerContextAssemblies = loader.CompilerLoadContext.Assemblies.SelectAsArray(a => a.FullName); try { diff --git a/src/Compilers/Core/CodeAnalysisTest/Text/SourceTextTests.cs b/src/Compilers/Core/CodeAnalysisTest/Text/SourceTextTests.cs index 5019fd668a2c..9dcd2e62a3a7 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Text/SourceTextTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Text/SourceTextTests.cs @@ -91,9 +91,23 @@ public void ChecksumAlgorithm_Default() Assert.Equal(SourceHashAlgorithm.Sha1, SourceText.From(stream).ChecksumAlgorithm); } + [Theory] + [InlineData(SourceHashAlgorithm.Sha1, "ff1816ec-aa5e-4d10-87f7-6f4963833460")] + [InlineData(SourceHashAlgorithm.Sha256, "8829d00f-11b8-4213-878b-770e8597ac16")] + [InlineData(SourceHashAlgorithm.Sha384, "d99cfeb1-8c43-444a-8a6c-b61269d2a0bf")] + [InlineData(SourceHashAlgorithm.Sha512, "ef2d1afc-6550-46d6-b14b-d70afe9a5566")] + public void ChecksumAlgorithm_GuidRoundTrip(SourceHashAlgorithm algorithm, string expectedGuid) + { + var guid = SourceHashAlgorithms.GetAlgorithmGuid(algorithm); + Assert.Equal(new Guid(expectedGuid), guid); + Assert.Equal(algorithm, SourceHashAlgorithms.GetSourceHashAlgorithm(guid)); + } + [Theory] [InlineData(SourceHashAlgorithm.Sha1)] [InlineData(SourceHashAlgorithm.Sha256)] + [InlineData(SourceHashAlgorithm.Sha384)] + [InlineData(SourceHashAlgorithm.Sha512)] public void ChecksumAlgorithm1(SourceHashAlgorithm algorithm) { Assert.Equal(algorithm, SourceText.From(HelloWorld, checksumAlgorithm: algorithm).ChecksumAlgorithm); @@ -249,7 +263,7 @@ public void ContentEqualsLarge() // Try all permutations of encodings and algorithms. None of them should affect the final result. var encodings = new[] { null, Encoding.ASCII, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false) }; - var hashAlgorithms = new[] { SourceHashAlgorithm.Sha1, SourceHashAlgorithm.Sha256 }; + var hashAlgorithms = new[] { SourceHashAlgorithm.Sha1, SourceHashAlgorithm.Sha256, SourceHashAlgorithm.Sha384, SourceHashAlgorithm.Sha512 }; var randomText = builder.ToString(); diff --git a/src/Compilers/Core/MSBuildTask/ManagedToolTask.cs b/src/Compilers/Core/MSBuildTask/ManagedToolTask.cs index cbfaedca8146..e16e3ae2caf3 100644 --- a/src/Compilers/Core/MSBuildTask/ManagedToolTask.cs +++ b/src/Compilers/Core/MSBuildTask/ManagedToolTask.cs @@ -268,6 +268,17 @@ .. Environment.GetEnvironmentVariables().Cast (path[path.Length - 1] == '/') ? path : path + '/'; + => EndsWithDirectorySeparator(path) ? path : path + Path.DirectorySeparatorChar; private static bool EndsWithDirectorySeparator(string path) { @@ -101,6 +108,9 @@ public override bool Execute() Log.LogErrorFromResources("MapSourceRoots.PathMustEndWithSlashOrBackslash", Names.SourceRoot, sourceRoot.ItemSpec); } + // Normalize the path. + sourceRoot.ItemSpec = NormalizePath(sourceRoot.ItemSpec); + if (rootByItemSpec.TryGetValue(sourceRoot.ItemSpec, out var existingRoot)) { ReportConflictingWellKnownMetadata(existingRoot, sourceRoot); @@ -164,17 +174,29 @@ void setTopLevelMappedPaths(bool sourceControlled) // finally, calculate mapped paths of nested roots: foreach (var root in mappedSourceRoots) { - string nestedRoot = root.GetMetadata(Names.NestedRoot); + string nestedRoot = Utilities.FixFilePath(root.GetMetadata(Names.NestedRoot)); if (!string.IsNullOrEmpty(nestedRoot)) { - string containingRoot = root.GetMetadata(Names.ContainingRoot); + string containingRoot = NormalizePath(Utilities.FixFilePath(root.GetMetadata(Names.ContainingRoot))); // The value of ContainingRoot metadata is a file path that is compared with ItemSpec values of SourceRoot items. // Since the paths in ItemSpec have backslashes replaced with slashes on non-Windows platforms we need to do the same for ContainingRoot. - if (containingRoot != null && topLevelMappedPaths.TryGetValue(Utilities.FixFilePath(containingRoot), out var mappedTopLevelPath)) + if (containingRoot != null && topLevelMappedPaths.TryGetValue(containingRoot, out var mappedTopLevelPath)) { + // Normalize nested root. + if (Utilities.TryCombine(containingRoot, nestedRoot, out var combinedPath)) + { + var fullOriginalPath = Utilities.GetFullPathNoThrow(combinedPath); + if (fullOriginalPath.StartsWith(containingRoot, StringComparison.OrdinalIgnoreCase)) + { + nestedRoot = fullOriginalPath.Substring(containingRoot.Length); + } + } + Debug.Assert(mappedTopLevelPath.EndsWith("/", StringComparison.Ordinal)); - root.SetMetadata(Names.MappedPath, mappedTopLevelPath + EnsureEndsWithSlash(nestedRoot.Replace('\\', '/'))); + root.SetMetadata(Names.MappedPath, mappedTopLevelPath + EnsureEndsWithSlash(nestedRoot).Replace('\\', '/')); + root.SetMetadata(Names.ContainingRoot, containingRoot); + root.SetMetadata(Names.NestedRoot, nestedRoot); } else { diff --git a/src/Compilers/Core/MSBuildTask/Microsoft.CSharp.Core.targets b/src/Compilers/Core/MSBuildTask/Microsoft.CSharp.Core.targets index c3a98f057cd1..ccb7c0b59689 100644 --- a/src/Compilers/Core/MSBuildTask/Microsoft.CSharp.Core.targets +++ b/src/Compilers/Core/MSBuildTask/Microsoft.CSharp.Core.targets @@ -59,6 +59,14 @@ @(CustomAdditionalCompileOutputs)" Returns="@(CscCommandLineArgs)" DependsOnTargets="$(CoreCompileDependsOn);_BeforeVBCSCoreCompile"> + + + + + + diff --git a/src/Compilers/Core/MSBuildTask/Microsoft.VisualBasic.Core.targets b/src/Compilers/Core/MSBuildTask/Microsoft.VisualBasic.Core.targets index 6b97be64b9bc..9b11fbba6285 100644 --- a/src/Compilers/Core/MSBuildTask/Microsoft.VisualBasic.Core.targets +++ b/src/Compilers/Core/MSBuildTask/Microsoft.VisualBasic.Core.targets @@ -30,6 +30,14 @@ @(CustomAdditionalCompileOutputs)" Returns="@(VbcCommandLineArgs)" DependsOnTargets="$(CoreCompileDependsOn);_BeforeVBCSCoreCompile"> + + + + + + <_NoWarnings Condition="'$(WarningLevel)' == '0'">true <_NoWarnings Condition="'$(WarningLevel)' == '1'">false diff --git a/src/Compilers/Core/MSBuildTask/Utilities.cs b/src/Compilers/Core/MSBuildTask/Utilities.cs index 46c9190c2636..68368148c102 100644 --- a/src/Compilers/Core/MSBuildTask/Utilities.cs +++ b/src/Compilers/Core/MSBuildTask/Utilities.cs @@ -6,6 +6,7 @@ using Microsoft.Build.Utilities; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Reflection; @@ -119,6 +120,20 @@ internal static string GetFullPathNoThrow(string path) return path; } + internal static bool TryCombine(string path1, string path2, [NotNullWhen(returnValue: true)] out string? combined) + { + try + { + combined = Path.Combine(path1, path2); + return true; + } + catch (Exception e) when (IsIoRelatedException(e)) + { + combined = null; + return false; + } + } + internal static void DeleteNoThrow(string path) { try diff --git a/src/Compilers/Core/MSBuildTaskTests/MapSourceRootTests.cs b/src/Compilers/Core/MSBuildTaskTests/MapSourceRootTests.cs index fe0e10edcc7f..32fb712b2a49 100644 --- a/src/Compilers/Core/MSBuildTaskTests/MapSourceRootTests.cs +++ b/src/Compilers/Core/MSBuildTaskTests/MapSourceRootTests.cs @@ -34,17 +34,17 @@ public void BasicMapping() BuildEngine = engine, SourceRoots = new[] { - new TaskItem(@"c:\packages\SourcePackage1\"), + new TaskItem(Utilities.FixFilePath(@"c:\packages\SourcePackage1\")), new TaskItem(@"/packages/SourcePackage2/"), - new TaskItem(@"c:\MyProjects\MyProject\", new Dictionary + new TaskItem(Utilities.FixFilePath(@"c:\MyProjects\MyProject\"), new Dictionary { { "SourceControl", "Git" }, }), - new TaskItem(@"c:\MyProjects\MyProject\a\b\", new Dictionary + new TaskItem(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\b\"), new Dictionary { { "SourceControl", "Git" }, { "NestedRoot", "a/b" }, - { "ContainingRoot", @"c:\MyProjects\MyProject\" }, + { "ContainingRoot", Utilities.FixFilePath(@"c:\MyProjects\MyProject\") }, { "some metadata", "some value" }, }), }, @@ -57,17 +57,17 @@ public void BasicMapping() RoslynDebug.Assert(task.MappedSourceRoots is object); Assert.Equal(4, task.MappedSourceRoots.Length); - Assert.Equal(Utilities.FixFilePath(@"c:\packages\SourcePackage1\"), task.MappedSourceRoots[0].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\packages\SourcePackage1\")), task.MappedSourceRoots[0].ItemSpec); Assert.Equal(@"/_1/", task.MappedSourceRoots[0].GetMetadata("MappedPath")); - Assert.Equal(Utilities.FixFilePath(@"/packages/SourcePackage2/"), task.MappedSourceRoots[1].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath("/packages/SourcePackage2/")), task.MappedSourceRoots[1].ItemSpec); Assert.Equal(@"/_2/", task.MappedSourceRoots[1].GetMetadata("MappedPath")); - Assert.Equal(Utilities.FixFilePath(@"c:\MyProjects\MyProject\"), task.MappedSourceRoots[2].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\MyProjects\MyProject\")), task.MappedSourceRoots[2].ItemSpec); Assert.Equal(@"/_/", task.MappedSourceRoots[2].GetMetadata("MappedPath")); Assert.Equal(@"Git", task.MappedSourceRoots[2].GetMetadata("SourceControl")); - Assert.Equal(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\b\"), task.MappedSourceRoots[3].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\b\")), task.MappedSourceRoots[3].ItemSpec); Assert.Equal(@"/_/a/b/", task.MappedSourceRoots[3].GetMetadata("MappedPath")); Assert.Equal(@"Git", task.MappedSourceRoots[3].GetMetadata("SourceControl")); Assert.Equal(@"some value", task.MappedSourceRoots[3].GetMetadata("some metadata")); @@ -106,14 +106,14 @@ public void InvalidChars() RoslynDebug.Assert(task.MappedSourceRoots is object); Assert.Equal(3, task.MappedSourceRoots.Length); - Assert.Equal(Utilities.FixFilePath(@"!@#:;$%^&*()_+|{}\"), task.MappedSourceRoots[0].ItemSpec); + Assert.Equal(Utilities.FixFilePath(Utilities.GetFullPathNoThrow(@"!@#:;$%^&*()_+|{}\")), task.MappedSourceRoots[0].ItemSpec); Assert.Equal(@"/_1/", task.MappedSourceRoots[0].GetMetadata("MappedPath")); - Assert.Equal(Utilities.FixFilePath("****/"), task.MappedSourceRoots[1].ItemSpec); + Assert.Equal(Utilities.FixFilePath(Utilities.GetFullPathNoThrow("****/")), task.MappedSourceRoots[1].ItemSpec); Assert.Equal(@"/_/", task.MappedSourceRoots[1].GetMetadata("MappedPath")); Assert.Equal(@"Git", task.MappedSourceRoots[1].GetMetadata("SourceControl")); - Assert.Equal(Utilities.FixFilePath(@"****\|||:;\"), task.MappedSourceRoots[2].ItemSpec); + Assert.Equal(Utilities.FixFilePath(Utilities.GetFullPathNoThrow(@"****\|||:;\")), task.MappedSourceRoots[2].ItemSpec); Assert.Equal(@"/_/|||:;/", task.MappedSourceRoots[2].GetMetadata("MappedPath")); Assert.Equal(@"Git", task.MappedSourceRoots[2].GetMetadata("SourceControl")); @@ -157,21 +157,21 @@ public void NestedRoots_Separators() BuildEngine = engine, SourceRoots = new[] { - new TaskItem(@"c:\MyProjects\MyProject\"), - new TaskItem(@"c:\MyProjects\MyProject\a\a\", new Dictionary + new TaskItem(Utilities.FixFilePath(@"c:\MyProjects\MyProject\")), + new TaskItem(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\a\"), new Dictionary { { "NestedRoot", @"a/a/" }, - { "ContainingRoot", @"c:\MyProjects\MyProject\" }, + { "ContainingRoot", Utilities.FixFilePath(@"c:\MyProjects\MyProject\") }, }), - new TaskItem(@"c:\MyProjects\MyProject\a\b\", new Dictionary + new TaskItem(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\b\"), new Dictionary { { "NestedRoot", @"a/b\" }, - { "ContainingRoot", @"c:\MyProjects\MyProject\" }, + { "ContainingRoot", Utilities.FixFilePath(@"c:\MyProjects\MyProject\") }, }), - new TaskItem(@"c:\MyProjects\MyProject\a\c\", new Dictionary + new TaskItem(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\c\"), new Dictionary { { "NestedRoot", @"a\c" }, - { "ContainingRoot", @"c:\MyProjects\MyProject\" }, + { "ContainingRoot", Utilities.FixFilePath(@"c:\MyProjects\MyProject\") }, }), }, Deterministic = true @@ -183,16 +183,16 @@ public void NestedRoots_Separators() RoslynDebug.Assert(task.MappedSourceRoots is object); Assert.Equal(4, task.MappedSourceRoots.Length); - Assert.Equal(Utilities.FixFilePath(@"c:\MyProjects\MyProject\"), task.MappedSourceRoots[0].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\MyProjects\MyProject\")), task.MappedSourceRoots[0].ItemSpec); Assert.Equal(@"/_/", task.MappedSourceRoots[0].GetMetadata("MappedPath")); - Assert.Equal(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\a\"), task.MappedSourceRoots[1].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\a\")), task.MappedSourceRoots[1].ItemSpec); Assert.Equal(@"/_/a/a/", task.MappedSourceRoots[1].GetMetadata("MappedPath")); - Assert.Equal(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\b\"), task.MappedSourceRoots[2].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\b\")), task.MappedSourceRoots[2].ItemSpec); Assert.Equal(@"/_/a/b/", task.MappedSourceRoots[2].GetMetadata("MappedPath")); - Assert.Equal(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\c\"), task.MappedSourceRoots[3].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\c\")), task.MappedSourceRoots[3].ItemSpec); Assert.Equal(@"/_/a/c/", task.MappedSourceRoots[3].GetMetadata("MappedPath")); Assert.True(result); @@ -208,9 +208,9 @@ public void SourceRootCaseSensitive() BuildEngine = engine, SourceRoots = new[] { - new TaskItem(@"c:\packages\SourcePackage1\"), - new TaskItem(@"C:\packages\SourcePackage1\"), - new TaskItem(@"c:\packages\SourcePackage2\"), + new TaskItem(Utilities.FixFilePath(@"c:\packages\SourcePackage1\")), + new TaskItem(Utilities.FixFilePath(@"C:\packages\SourcePackage1\")), + new TaskItem(Utilities.FixFilePath(@"c:\packages\SourcePackage2\")), }, Deterministic = true }; @@ -221,13 +221,13 @@ public void SourceRootCaseSensitive() RoslynDebug.Assert(task.MappedSourceRoots is object); Assert.Equal(3, task.MappedSourceRoots.Length); - Assert.Equal(Utilities.FixFilePath(@"c:\packages\SourcePackage1\"), task.MappedSourceRoots[0].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\packages\SourcePackage1\")), task.MappedSourceRoots[0].ItemSpec); Assert.Equal(@"/_/", task.MappedSourceRoots[0].GetMetadata("MappedPath")); - Assert.Equal(Utilities.FixFilePath(@"C:\packages\SourcePackage1\"), task.MappedSourceRoots[1].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"C:\packages\SourcePackage1\")), task.MappedSourceRoots[1].ItemSpec); Assert.Equal(@"/_1/", task.MappedSourceRoots[1].GetMetadata("MappedPath")); - Assert.Equal(Utilities.FixFilePath(@"c:\packages\SourcePackage2\"), task.MappedSourceRoots[2].ItemSpec); + Assert.Equal(Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\packages\SourcePackage2\")), task.MappedSourceRoots[2].ItemSpec); Assert.Equal(@"/_2/", task.MappedSourceRoots[2].GetMetadata("MappedPath")); Assert.True(result); @@ -266,9 +266,9 @@ public void Error_Recursion() AssertEx.AssertEqualToleratingWhitespaceDifferences( "ERROR : " + string.Format(task.Log.FormatResourceString( - "MapSourceRoots.NoSuchTopLevelSourceRoot", "SourceRoot.ContainingRoot", "SourceRoot", path2)) + Environment.NewLine + + "MapSourceRoots.NoSuchTopLevelSourceRoot", "SourceRoot.ContainingRoot", "SourceRoot", Utilities.GetFullPathNoThrow(path2))) + Environment.NewLine + "ERROR : " + string.Format(task.Log.FormatResourceString( - "MapSourceRoots.NoSuchTopLevelSourceRoot", "SourceRoot.ContainingRoot", "SourceRoot", path1)) + Environment.NewLine, engine.Log); + "MapSourceRoots.NoSuchTopLevelSourceRoot", "SourceRoot.ContainingRoot", "SourceRoot", Utilities.GetFullPathNoThrow(path1))) + Environment.NewLine, engine.Log); Assert.Null(task.MappedSourceRoots); Assert.False(result); @@ -327,26 +327,26 @@ public void MetadataMerge1(bool deterministic) AssertEx.AssertEqualToleratingWhitespaceDifferences( "WARNING : " + string.Format(task.Log.FormatResourceString( - "MapSourceRoots.ContainsDuplicate", "SourceRoot", path1, "SourceControl", "git", "tfvc")) + Environment.NewLine + + "MapSourceRoots.ContainsDuplicate", "SourceRoot", Utilities.GetFullPathNoThrow(path1), "SourceControl", "git", "tfvc")) + Environment.NewLine + "WARNING : " + string.Format(task.Log.FormatResourceString( - "MapSourceRoots.ContainsDuplicate", "SourceRoot", path1, "RevisionId", "RevId1", "RevId2")) + Environment.NewLine + + "MapSourceRoots.ContainsDuplicate", "SourceRoot", Utilities.GetFullPathNoThrow(path1), "RevisionId", "RevId1", "RevId2")) + Environment.NewLine + "WARNING : " + string.Format(task.Log.FormatResourceString( - "MapSourceRoots.ContainsDuplicate", "SourceRoot", path1, "NestedRoot", "NR1A", "NR1B")) + Environment.NewLine + + "MapSourceRoots.ContainsDuplicate", "SourceRoot", Utilities.GetFullPathNoThrow(path1), "NestedRoot", "NR1A", "NR1B")) + Environment.NewLine + "WARNING : " + string.Format(task.Log.FormatResourceString( - "MapSourceRoots.ContainsDuplicate", "SourceRoot", path1, "ContainingRoot", path3, "CR")) + Environment.NewLine + + "MapSourceRoots.ContainsDuplicate", "SourceRoot", Utilities.GetFullPathNoThrow(path1), "ContainingRoot", path3, "CR")) + Environment.NewLine + "WARNING : " + string.Format(task.Log.FormatResourceString( - "MapSourceRoots.ContainsDuplicate", "SourceRoot", path1, "MappedPath", "MP1", "MP2")) + Environment.NewLine + + "MapSourceRoots.ContainsDuplicate", "SourceRoot", Utilities.GetFullPathNoThrow(path1), "MappedPath", "MP1", "MP2")) + Environment.NewLine + "WARNING : " + string.Format(task.Log.FormatResourceString( - "MapSourceRoots.ContainsDuplicate", "SourceRoot", path1, "SourceLinkUrl", "URL1", "URL2")) + Environment.NewLine, + "MapSourceRoots.ContainsDuplicate", "SourceRoot", Utilities.GetFullPathNoThrow(path1), "SourceLinkUrl", "URL1", "URL2")) + Environment.NewLine, engine.Log); AssertEx.NotNull(task.MappedSourceRoots); - AssertEx.Equal(new[] - { - $"'{path1}' SourceControl='git' RevisionId='RevId1' NestedRoot='NR1A' ContainingRoot='{path3}' MappedPath='{(deterministic ? "/_/NR1A/" : path1)}' SourceLinkUrl='URL1'", - $"'{path2}' SourceControl='git' RevisionId='' NestedRoot='NR2' ContainingRoot='{path3}' MappedPath='{(deterministic ? "/_/NR2/" : path2)}' SourceLinkUrl=''", - $"'{path3}' SourceControl='' RevisionId='' NestedRoot='' ContainingRoot='' MappedPath='{(deterministic ? "/_/" : path3)}' SourceLinkUrl=''", - }, task.MappedSourceRoots.Select(InspectSourceRoot)); + AssertEx.Equal(string.Join("\n", + [ + $"'{Utilities.GetFullPathNoThrow(path1)}' SourceControl='git' RevisionId='RevId1' NestedRoot='NR1A' ContainingRoot='{(deterministic ? Utilities.GetFullPathNoThrow(path3) : path3)}' MappedPath='{(deterministic ? "/_/NR1A/" : Utilities.GetFullPathNoThrow(path1))}' SourceLinkUrl='URL1'", + $"'{Utilities.GetFullPathNoThrow(path2)}' SourceControl='git' RevisionId='' NestedRoot='NR2' ContainingRoot='{(deterministic ? Utilities.GetFullPathNoThrow(path3) : path3)}' MappedPath='{(deterministic ? "/_/NR2/" : Utilities.GetFullPathNoThrow(path2))}' SourceLinkUrl=''", + $"'{Utilities.GetFullPathNoThrow(path3)}' SourceControl='' RevisionId='' NestedRoot='' ContainingRoot='' MappedPath='{(deterministic ? "/_/" : Utilities.GetFullPathNoThrow(path3))}' SourceLinkUrl=''", + ]), string.Join("\n", task.MappedSourceRoots.Select(InspectSourceRoot))); Assert.True(result); } @@ -361,12 +361,12 @@ public void Error_MissingContainingRoot() BuildEngine = engine, SourceRoots = new[] { - new TaskItem(@"c:\MyProjects\MYPROJECT\"), - new TaskItem(@"c:\MyProjects\MyProject\a\b\", new Dictionary + new TaskItem(Utilities.FixFilePath(@"c:\MyProjects\MYPROJECT\")), + new TaskItem(Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\b\"), new Dictionary { { "SourceControl", "Git" }, { "NestedRoot", "a/b" }, - { "ContainingRoot", @"c:\MyProjects\MyProject\" }, + { "ContainingRoot", Utilities.FixFilePath(@"c:\MyProjects\MyProject\") }, }), }, Deterministic = true @@ -375,7 +375,7 @@ public void Error_MissingContainingRoot() bool result = task.Execute(); AssertEx.AssertEqualToleratingWhitespaceDifferences("ERROR : " + string.Format(task.Log.FormatResourceString( - "MapSourceRoots.NoSuchTopLevelSourceRoot", "SourceRoot.ContainingRoot", "SourceRoot", @"c:\MyProjects\MyProject\")) + Environment.NewLine, engine.Log); + "MapSourceRoots.NoSuchTopLevelSourceRoot", "SourceRoot.ContainingRoot", "SourceRoot", Utilities.GetFullPathNoThrow(Utilities.FixFilePath(@"c:\MyProjects\MyProject\")))) + Environment.NewLine, engine.Log); Assert.Null(task.MappedSourceRoots); Assert.False(result); @@ -446,14 +446,58 @@ public void Error_NoTopLevelSourceRoot(bool deterministic) else { AssertEx.NotNull(task.MappedSourceRoots); - AssertEx.Equal(new[] - { - $"'{path1}' SourceControl='' RevisionId='' NestedRoot='a/b' ContainingRoot='{path1}' MappedPath='{path1}' SourceLinkUrl=''", - }, task.MappedSourceRoots.Select(InspectSourceRoot)); + AssertEx.Equal(string.Join("\n", + [ + $"'{Utilities.GetFullPathNoThrow(path1)}' SourceControl='' RevisionId='' NestedRoot='a/b' ContainingRoot='{(deterministic ? Utilities.GetFullPathNoThrow(path1) : path1)}' MappedPath='{Utilities.GetFullPathNoThrow(path1)}' SourceLinkUrl=''", + ]), string.Join("\n", task.MappedSourceRoots.Select(InspectSourceRoot))); Assert.True(result); } } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/82112")] + public void NormalizePaths(bool deterministic, [CombinatorialValues("e", "d/../e")] string nestedRoot) + { + var engine = new MockEngine(); + + var originalPath1 = Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\b\..\c\"); + var normalizedPath1 = Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\c\"); + var originalPath2 = Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\b\..\c\d\..\e\"); + var normalizedPath2 = Utilities.FixFilePath(@"c:\MyProjects\MyProject\a\c\e\"); + + var task = new MapSourceRoots + { + BuildEngine = engine, + SourceRoots = + [ + new TaskItem(originalPath1), + new TaskItem(originalPath2, new Dictionary + { + { "ContainingRoot", originalPath1 }, + { "NestedRoot", nestedRoot }, + }), + ], + Deterministic = deterministic, + }; + + bool result = task.Execute(); + AssertEx.AssertEqualToleratingWhitespaceDifferences("", engine.Log); + + var expectedMappedPath1 = deterministic ? "/_/" : Utilities.GetFullPathNoThrow(normalizedPath1); + var expectedNestedRoot = deterministic ? "e" : nestedRoot; + var expectedContainingRoot = deterministic ? Utilities.GetFullPathNoThrow(normalizedPath1) : originalPath1; + var expectedMappedPath2 = deterministic ? "/_/e/" : Utilities.GetFullPathNoThrow(normalizedPath2); + + AssertEx.NotNull(task.MappedSourceRoots); + AssertEx.Equal( + $""" + '{Utilities.GetFullPathNoThrow(normalizedPath1)}' SourceControl='' RevisionId='' NestedRoot='' ContainingRoot='' MappedPath='{expectedMappedPath1}' SourceLinkUrl='' + '{Utilities.GetFullPathNoThrow(normalizedPath2)}' SourceControl='' RevisionId='' NestedRoot='{expectedNestedRoot}' ContainingRoot='{expectedContainingRoot}' MappedPath='{expectedMappedPath2}' SourceLinkUrl='' + """, + string.Join(Environment.NewLine, task.MappedSourceRoots.Select(InspectSourceRoot))); + + Assert.True(result); + } } } diff --git a/src/Compilers/Core/MSBuildTaskTests/SdkIntegrationTests.cs b/src/Compilers/Core/MSBuildTaskTests/SdkIntegrationTests.cs index dfe22ed41599..7db12af43ede 100644 --- a/src/Compilers/Core/MSBuildTaskTests/SdkIntegrationTests.cs +++ b/src/Compilers/Core/MSBuildTaskTests/SdkIntegrationTests.cs @@ -7,12 +7,12 @@ using System.IO; using System.Linq; using System.Text; -using System.Threading.Tasks; +using Basic.CompilerLog.Util; +using Microsoft.Build.Logging.StructuredLogger; using Microsoft.CodeAnalysis.Test.Utilities; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; -using Basic.CompilerLog.Util; namespace Microsoft.CodeAnalysis.BuildTasks.UnitTests; @@ -200,4 +200,79 @@ End Module ArtifactUploadUtil.SetSucceeded(); } + + [ConditionalFact(typeof(DotNetSdkAvailable))] + [WorkItem("https://github.com/dotnet/roslyn/issues/82721")] + public void EditorConfig_EmbeddedInBinlog_Generated() + { + var projectFile = ProjectDir.CreateFile("console.csproj"); + projectFile.WriteAllText($""" + + + {NetCoreTfm} + + + + + + + """); + + ProjectDir.CreateFile("c.cs").WriteAllText(""" + class C { } + """); + + var binlogPath = RunBuild(projectFile.Path); + + var build = BinaryLog.ReadBuild(binlogPath); + + string embeddedText = build.SourceFiles + .Single(static f => f.FullPath.EndsWith("GeneratedMSBuildEditorConfig.editorconfig", StringComparison.OrdinalIgnoreCase)) + .Text; + + Assert.Contains("is_global = true", embeddedText); + Assert.Contains("build_property.RootNamespace", embeddedText); + ArtifactUploadUtil.SetSucceeded(); + } + + [ConditionalFact(typeof(DotNetSdkAvailable))] + [WorkItem("https://github.com/dotnet/roslyn/issues/82721")] + public void EditorConfig_EmbeddedInBinlog_FromTarget() + { + string analyzerGlobalConfigText = """ + is_global = true + some_prop = some_val + """; + var analyzerGlobalConfig = ProjectDir.CreateFile("analyzer.globalconfig").WriteAllText(analyzerGlobalConfigText); + + var projectFile = ProjectDir.CreateFile("console.csproj"); + projectFile.WriteAllText($""" + + + {NetCoreTfm} + + + + + + + + + """); + + ProjectDir.CreateFile("c.cs").WriteAllText(""" + class C { } + """); + + var binlogPath = RunBuild(projectFile.Path); + + var build = BinaryLog.ReadBuild(binlogPath); + + string embeddedText = build.SourceFiles + .Single(f => f.FullPath.Equals(analyzerGlobalConfig.Path, StringComparison.OrdinalIgnoreCase)) + .Text; + + Assert.Equal(analyzerGlobalConfigText, embeddedText); + ArtifactUploadUtil.SetSucceeded(); + } } diff --git a/src/Compilers/Core/MSBuildTaskTests/TestUtilities/DotNetSdkTestBase.cs b/src/Compilers/Core/MSBuildTaskTests/TestUtilities/DotNetSdkTestBase.cs index 61e4aca2c4cd..c1e9103bd944 100644 --- a/src/Compilers/Core/MSBuildTaskTests/TestUtilities/DotNetSdkTestBase.cs +++ b/src/Compilers/Core/MSBuildTaskTests/TestUtilities/DotNetSdkTestBase.cs @@ -133,6 +133,9 @@ private static void EmitTestHelperTargets( + + + diff --git a/src/Compilers/Core/Portable/CodeAnalysisResources.resx b/src/Compilers/Core/Portable/CodeAnalysisResources.resx index c95a774bab60..04ab810e7255 100644 --- a/src/Compilers/Core/Portable/CodeAnalysisResources.resx +++ b/src/Compilers/Core/Portable/CodeAnalysisResources.resx @@ -134,21 +134,25 @@ class + Refers to a class type in programming (noun), not the verb "to classify" attribute + Refers to a programming attribute/annotation (noun), not the verb "to attribute" constructor delegate + Refers to a delegate type in programming (noun), not the verb "to delegate" enum event + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -158,6 +162,7 @@ interface + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -173,6 +178,7 @@ return + Refers to a method return value/type (noun), not the verb "to return" struct @@ -495,6 +501,15 @@ NOTE: Elapsed time may be less than analyzer execution time because analyzers can run concurrently. + + Analyzers that have not enabled concurrent execution: + + + All analyzers have enabled concurrent execution. + + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + Time (s) @@ -734,4 +749,4 @@ {0}; file may be locked by {1} - \ No newline at end of file + diff --git a/src/Compilers/Core/Portable/CommandLine/CommandLineParser.cs b/src/Compilers/Core/Portable/CommandLine/CommandLineParser.cs index 64b37f79b9f8..7cfffda0987b 100644 --- a/src/Compilers/Core/Portable/CommandLine/CommandLineParser.cs +++ b/src/Compilers/Core/Portable/CommandLine/CommandLineParser.cs @@ -24,7 +24,6 @@ public abstract class CommandLineParser internal readonly bool IsScriptCommandLineParser; private static readonly char[] s_searchPatternTrimChars = new char[] { '\t', '\n', '\v', '\f', '\r', ' ', '\x0085', '\x00a0' }; internal const string ErrorLogOptionFormat = "[,version={1|1.0|2|2.1}]"; - private static bool s_registeredEncodingProvider = CodePagesEncodingProvider.Instance == null; internal CommandLineParser(CommonMessageProvider messageProvider, bool isScriptCommandLineParser) { @@ -1216,79 +1215,9 @@ internal IEnumerable ParseRecurseArgument(string arg, str } internal static Encoding? TryParseEncodingName(string arg) - { - if (!string.IsNullOrWhiteSpace(arg) - && long.TryParse(arg, NumberStyles.None, CultureInfo.InvariantCulture, out long codepage) - && (codepage > 0)) - { -try_again: - try - { - return Encoding.GetEncoding((int)codepage); - } - catch (NotSupportedException) when (!s_registeredEncodingProvider) - { - // From documentation: - // - 'GetEncoding' throws NotSupportedException when codepage is not supported by the underlying platform. - // - 'EncodingProvider.Instance' gets an encoding provider for code pages supported - // in the desktop .NET Framework but not by the current underlying platform. - // - 'Encoding.RegisterProvider' makes character encodings available on a platform that does not otherwise support them. - // * Once the encoding provider is registered, the encodings that it supports can be retrieved by calling any - // Encoding.GetEncoding overload. - // * Registering an encoding provider by using the 'RegisterProvider' method also affects the behavior of - // GetEncoding(Int32) when passed an argument of 0. - // * If multiple providers are registered, GetEncoding(Int32) attempts to retrieve the encoding from the most recently - // registered provider first. - // * If the 'RegisterProvider' method is called to register multiple providers that handle the same encoding, - // the last registered provider is the used for all encoding and decoding operations. Any previously registered providers are ignored. - // * If the same encoding provider is used in multiple calls to the 'RegisterProvider' method, - // only the first method call registers the provider. Subsequent calls are ignored. - // - // Given all that: - // - We don't call 'Encoding.RegisterProvider' unconditionally to avoid changing environment - // that is already configured to support the requested codepage. We call it only when we encounter - // a 'NotSupportedException'. - // - We also do not attempt to call 'Encoding.RegisterProvider' more than once. - try - { - // Ignore any exceptions from an attempt to register the provider. - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - } - catch - { - } - - s_registeredEncodingProvider = true; - - // Try to get the encoding again after attempting to register the provider. - // Since we set `s_registeredEncodingProvider` to true, we won't get here again. - goto try_again; - } - catch (Exception) - { - return null; - } - } - - return null; - } - - internal static SourceHashAlgorithm TryParseHashAlgorithmName(string arg) - { - if (string.Equals("sha1", arg, StringComparison.OrdinalIgnoreCase)) - { - return SourceHashAlgorithm.Sha1; - } - - if (string.Equals("sha256", arg, StringComparison.OrdinalIgnoreCase)) - { - return SourceHashAlgorithm.Sha256; - } - - // MD5 is legacy, not supported - - return SourceHashAlgorithm.None; - } + => int.TryParse(arg, NumberStyles.None, CultureInfo.InvariantCulture, out var codepage) && codepage > 0 + ? EncodedStringText.TryGetCodePageEncoding(codepage) + : null; private IEnumerable ExpandFileNamePattern( string path, diff --git a/src/Compilers/Core/Portable/CommandLine/Feature.cs b/src/Compilers/Core/Portable/CommandLine/Feature.cs index 3a29ce1413fe..4e3ef445d4fd 100644 --- a/src/Compilers/Core/Portable/CommandLine/Feature.cs +++ b/src/Compilers/Core/Portable/CommandLine/Feature.cs @@ -13,6 +13,7 @@ internal static class Feature { internal const string Strict = "strict"; internal const string UseLegacyStrongNameProvider = "UseLegacyStrongNameProvider"; + internal const string UpdatedMemorySafetyRules = "updated-memory-safety-rules"; internal const string EnableGeneratorCache = "enable-generator-cache"; internal const string PdbPathDeterminism = "pdb-path-determinism"; internal const string DebugDeterminism = "debug-determinism"; diff --git a/src/Compilers/Core/Portable/CommandLine/ReportAnalyzerUtil.cs b/src/Compilers/Core/Portable/CommandLine/ReportAnalyzerUtil.cs index 218fc34d16cd..2f8984a3654f 100644 --- a/src/Compilers/Core/Portable/CommandLine/ReportAnalyzerUtil.cs +++ b/src/Compilers/Core/Portable/CommandLine/ReportAnalyzerUtil.cs @@ -8,8 +8,8 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Text; using Microsoft.CodeAnalysis.Diagnostics; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis { @@ -31,6 +31,7 @@ public static void Report( if (analyzerDriver is { }) { ReportAnalyzerExecutionTime(consoleOutput, analyzerDriver, culture); + ReportNonConcurrentAnalyzers(consoleOutput, analyzerDriver); } if (driverTimingInfo is { } info) @@ -109,6 +110,42 @@ private static IEnumerable GetSupportedIds(DiagnosticAnalyzer analyzer) _ => analyzer.SupportedDiagnostics.Select(d => d.Id), }; + private static void ReportNonConcurrentAnalyzers(TextWriter consoleOutput, AnalyzerDriver analyzerDriver) + { + // DiagnosticSuppressors are never concurrent. To reduce noise in the report, only their count is reported. + var nonConcurrentAnalyzersAndSuppressors = analyzerDriver.NonConcurrentAnalyzers; + var nonConcurrentAnalyzers = nonConcurrentAnalyzersAndSuppressors.WhereAsArray(a => a is not DiagnosticSuppressor); + var suppressorCount = nonConcurrentAnalyzersAndSuppressors.Count - nonConcurrentAnalyzers.Length; + if (suppressorCount > 0) + { + consoleOutput.WriteLine(string.Format(CodeAnalysisResources.SuppressorsNonConcurrentCountMessage, suppressorCount)); + } + + if (nonConcurrentAnalyzers.Length == 0) + { + consoleOutput.WriteLine(CodeAnalysisResources.AllAnalyzersConcurrentMessage); + consoleOutput.WriteLine(); + return; + } + + consoleOutput.WriteLine(CodeAnalysisResources.NonConcurrentAnalyzersHeader); + + var byAssembly = nonConcurrentAnalyzers + .GroupBy(a => a.GetType().Assembly) + .OrderBy(g => g.Key.FullName, StringComparer.OrdinalIgnoreCase); + + foreach (var group in byAssembly) + { + consoleOutput.WriteLine($" {group.Key.FullName}"); + foreach (var analyzer in group.OrderBy(a => a.GetType().FullName, StringComparer.OrdinalIgnoreCase)) + { + consoleOutput.WriteLine($" {analyzer.GetType().FullName}"); + } + } + + consoleOutput.WriteLine(); + } + private static void ReportGeneratorExecutionTime(TextWriter consoleOutput, GeneratorDriverTimingInfo driverTimingInfo, CultureInfo culture) { if (driverTimingInfo.GeneratorTimes.IsEmpty) diff --git a/src/Compilers/Core/Portable/Compilation/DeterministicKeyBuilder.cs b/src/Compilers/Core/Portable/Compilation/DeterministicKeyBuilder.cs index d7f55198d689..728482ed5b1b 100644 --- a/src/Compilers/Core/Portable/Compilation/DeterministicKeyBuilder.cs +++ b/src/Compilers/Core/Portable/Compilation/DeterministicKeyBuilder.cs @@ -251,12 +251,33 @@ private void WriteCompilation( writer.WriteKey("options"); WriteCompilationOptions(writer, compilationOptions); + // Collect unique ParseOptions from all syntax trees and write them as a top-level array. + // Each syntax tree will reference its ParseOptions by index into this array, avoiding + // repetition when all (or most) trees share the same options. + var parseOptionsList = new List(); + foreach (var syntaxTree in syntaxTrees) + { + var treeOptions = syntaxTree.Options; + if (!parseOptionsList.Contains(treeOptions)) + { + parseOptionsList.Add(treeOptions); + } + } + + writer.WriteKey("parseOptions"); + writer.WriteArrayStart(); + foreach (var parseOptions in parseOptionsList) + { + WriteParseOptions(writer, parseOptions); + } + writer.WriteArrayEnd(); + writer.WriteKey("syntaxTrees"); writer.WriteArrayStart(); foreach (var syntaxTree in syntaxTrees) { cancellationToken.ThrowIfCancellationRequested(); - WriteSyntaxTree(writer, syntaxTree, pathMap, options, cancellationToken); + WriteSyntaxTree(writer, syntaxTree, parseOptionsList.IndexOf(syntaxTree.Options), pathMap, options, cancellationToken); } writer.WriteArrayEnd(); @@ -293,6 +314,7 @@ void writeToolsVersions() private void WriteSyntaxTree( JsonWriter writer, SyntaxTreeKey syntaxTree, + int parseOptionsIndex, ImmutableArray> pathMap, DeterministicKeyOptions options, CancellationToken cancellationToken) @@ -301,8 +323,7 @@ private void WriteSyntaxTree( WriteFilePath(writer, "fileName", syntaxTree.FilePath, pathMap, options); writer.WriteKey("text"); WriteSourceText(writer, syntaxTree.GetText(cancellationToken)); - writer.WriteKey("parseOptions"); - WriteParseOptions(writer, syntaxTree.Options); + writer.Write("parseOptionsIndex", parseOptionsIndex); writer.WriteObjectEnd(); } diff --git a/src/Compilers/Core/Portable/CryptographicHashProvider.cs b/src/Compilers/Core/Portable/CryptographicHashProvider.cs index c74d31e51cd0..f935b8a42f34 100644 --- a/src/Compilers/Core/Portable/CryptographicHashProvider.cs +++ b/src/Compilers/Core/Portable/CryptographicHashProvider.cs @@ -72,6 +72,12 @@ internal static int GetHashSize(SourceHashAlgorithm algorithmId) case SourceHashAlgorithm.Sha256: return 256 / 8; + case SourceHashAlgorithm.Sha384: + return 384 / 8; + + case SourceHashAlgorithm.Sha512: + return 512 / 8; + default: throw ExceptionUtilities.UnexpectedValue(algorithmId); } @@ -88,6 +94,12 @@ internal static int GetHashSize(SourceHashAlgorithm algorithmId) case SourceHashAlgorithm.Sha256: return SHA256.Create(); + case SourceHashAlgorithm.Sha384: + return SHA384.Create(); + + case SourceHashAlgorithm.Sha512: + return SHA512.Create(); + default: return null; } @@ -104,6 +116,12 @@ internal static HashAlgorithmName GetAlgorithmName(SourceHashAlgorithm algorithm case SourceHashAlgorithm.Sha256: return HashAlgorithmName.SHA256; + case SourceHashAlgorithm.Sha384: + return HashAlgorithmName.SHA384; + + case SourceHashAlgorithm.Sha512: + return HashAlgorithmName.SHA512; + default: throw ExceptionUtilities.UnexpectedValue(algorithmId); } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisResultBuilder.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisResultBuilder.cs index d48ff0662983..30c5fd7af22d 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisResultBuilder.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalysisResultBuilder.cs @@ -506,7 +506,9 @@ internal AnalysisResult ToAnalysisResult(ImmutableArray anal ImmutableDictionary>> localAdditionalFileDiagnostics; ImmutableDictionary> nonLocalDiagnostics; - var analyzersSet = analyzers.ToImmutableHashSet(); + var analyzersSet = PooledHashSet.GetInstance(); + analyzersSet.AddRange(analyzers); + Func shouldInclude = analysisScope.ShouldInclude; lock (_gate) { @@ -516,13 +518,20 @@ internal AnalysisResult ToAnalysisResult(ImmutableArray anal nonLocalDiagnostics = GetImmutable(analyzersSet, shouldInclude, _nonLocalDiagnosticsOpt); } + analyzersSet.Free(); cancellationToken.ThrowIfCancellationRequested(); var analyzerTelemetryInfo = GetTelemetryInfo(analyzers); return new AnalysisResult(analyzers, localSyntaxDiagnostics, localSemanticDiagnostics, localAdditionalFileDiagnostics, nonLocalDiagnostics, analyzerTelemetryInfo); } + /// + /// Gets an immutable dictionary from the given local diagnostics map. + /// + /// Limits the analyzers included. Is not modified. + /// Filter determining whether a diagnostic should be included + /// Diagnostic map to operate on private static ImmutableDictionary>> GetImmutable( - ImmutableHashSet analyzers, + HashSet analyzers, // Will not be modified Func shouldInclude, Dictionary.Builder>>? localDiagnosticsOpt) where TKey : class @@ -557,8 +566,14 @@ private static ImmutableDictionary + /// Gets an immutable dictionary from the given non-local diagnostics map. + /// + /// Limits the analyzers included. Is not modified. + /// Filter determining whether a diagnostic should be included + /// Diagnostic map to operate on private static ImmutableDictionary> GetImmutable( - ImmutableHashSet analyzers, + HashSet analyzers, Func shouldInclude, Dictionary.Builder>? nonLocalDiagnosticsOpt) { diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs index 134ee76bc9da..491f4ace8276 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerDriver.cs @@ -1460,6 +1460,11 @@ public Task WhenCompletedTask internal ImmutableDictionary AnalyzerExecutionTimes => AnalyzerExecutor.AnalyzerExecutionTimes; internal TimeSpan ResetAnalyzerExecutionTime(DiagnosticAnalyzer analyzer) => AnalyzerExecutor.ResetAnalyzerExecutionTime(analyzer); + /// + /// Returns the set of analyzers that did not call . + /// + internal IReadOnlyCollection NonConcurrentAnalyzers => AnalyzerGateMap.Keys; + private static ImmutableArray<(DiagnosticAnalyzer, ImmutableArray>)> MakeSymbolActionsByKind(in AnalyzerActions analyzerActions) { var builder = ArrayBuilder<(DiagnosticAnalyzer, ImmutableArray>)>.GetInstance(); @@ -2043,7 +2048,7 @@ internal static Action.GetInstance(); foreach (var analyzer in analyzers) { @@ -2052,13 +2057,13 @@ internal static Action operationBlockActions, ImmutableArray syntaxNodeActions, ImmutableArray operationActions, - bool concurrent, - bool isEmpty) + bool concurrent) { _compilationStartActions = compilationStartActions; _compilationEndActions = compilationEndActions; @@ -753,7 +752,24 @@ public AnalyzerActions( _syntaxNodeActions = syntaxNodeActions; _operationActions = operationActions; _concurrent = concurrent; - IsEmpty = isEmpty; + + IsEmpty = compilationStartActions.IsEmpty && + compilationEndActions.IsEmpty && + compilationActions.IsEmpty && + syntaxTreeActions.IsEmpty && + additionalFileActions.IsEmpty && + semanticModelActions.IsEmpty && + symbolActions.IsEmpty && + symbolStartActions.IsEmpty && + symbolEndActions.IsEmpty && + codeBlockStartActions.IsEmpty && + codeBlockEndActions.IsEmpty && + codeBlockActions.IsEmpty && + operationBlockStartActions.IsEmpty && + operationBlockEndActions.IsEmpty && + operationBlockActions.IsEmpty && + syntaxNodeActions.IsEmpty && + operationActions.IsEmpty; } public readonly int CompilationStartActionsCount { get { return _compilationStartActions.Length; } } @@ -999,27 +1015,185 @@ public readonly AnalyzerActions Append(in AnalyzerActions otherActions, bool app throw new ArgumentNullException(nameof(otherActions)); } - AnalyzerActions actions = new AnalyzerActions(concurrent: _concurrent || otherActions.Concurrent); - actions._compilationStartActions = _compilationStartActions.AddRange(otherActions._compilationStartActions); - actions._compilationEndActions = _compilationEndActions.AddRange(otherActions._compilationEndActions); - actions._compilationActions = _compilationActions.AddRange(otherActions._compilationActions); - actions._syntaxTreeActions = _syntaxTreeActions.AddRange(otherActions._syntaxTreeActions); - actions._additionalFileActions = _additionalFileActions.AddRange(otherActions._additionalFileActions); - actions._semanticModelActions = _semanticModelActions.AddRange(otherActions._semanticModelActions); - actions._symbolActions = _symbolActions.AddRange(otherActions._symbolActions); - actions._symbolStartActions = appendSymbolStartAndSymbolEndActions ? _symbolStartActions.AddRange(otherActions._symbolStartActions) : _symbolStartActions; - actions._symbolEndActions = appendSymbolStartAndSymbolEndActions ? _symbolEndActions.AddRange(otherActions._symbolEndActions) : _symbolEndActions; - actions._codeBlockStartActions = _codeBlockStartActions.AddRange(otherActions._codeBlockStartActions); - actions._codeBlockEndActions = _codeBlockEndActions.AddRange(otherActions._codeBlockEndActions); - actions._codeBlockActions = _codeBlockActions.AddRange(otherActions._codeBlockActions); - actions._syntaxNodeActions = _syntaxNodeActions.AddRange(otherActions._syntaxNodeActions); - actions._operationActions = _operationActions.AddRange(otherActions._operationActions); - actions._operationBlockStartActions = _operationBlockStartActions.AddRange(otherActions._operationBlockStartActions); - actions._operationBlockEndActions = _operationBlockEndActions.AddRange(otherActions._operationBlockEndActions); - actions._operationBlockActions = _operationBlockActions.AddRange(otherActions._operationBlockActions); - actions.IsEmpty = IsEmpty && otherActions.IsEmpty; + AnalyzerActions actions = new AnalyzerActions( + compilationStartActions: _compilationStartActions.AddRange(otherActions._compilationStartActions), + compilationEndActions: _compilationEndActions.AddRange(otherActions._compilationEndActions), + compilationActions: _compilationActions.AddRange(otherActions._compilationActions), + syntaxTreeActions: _syntaxTreeActions.AddRange(otherActions._syntaxTreeActions), + additionalFileActions: _additionalFileActions.AddRange(otherActions._additionalFileActions), + semanticModelActions: _semanticModelActions.AddRange(otherActions._semanticModelActions), + symbolActions: _symbolActions.AddRange(otherActions._symbolActions), + symbolStartActions: appendSymbolStartAndSymbolEndActions ? _symbolStartActions.AddRange(otherActions._symbolStartActions) : _symbolStartActions, + symbolEndActions: appendSymbolStartAndSymbolEndActions ? _symbolEndActions.AddRange(otherActions._symbolEndActions) : _symbolEndActions, + codeBlockStartActions: _codeBlockStartActions.AddRange(otherActions._codeBlockStartActions), + codeBlockEndActions: _codeBlockEndActions.AddRange(otherActions._codeBlockEndActions), + codeBlockActions: _codeBlockActions.AddRange(otherActions._codeBlockActions), + operationBlockStartActions: _operationBlockStartActions.AddRange(otherActions._operationBlockStartActions), + operationBlockEndActions: _operationBlockEndActions.AddRange(otherActions._operationBlockEndActions), + operationBlockActions: _operationBlockActions.AddRange(otherActions._operationBlockActions), + syntaxNodeActions: _syntaxNodeActions.AddRange(otherActions._syntaxNodeActions), + operationActions: _operationActions.AddRange(otherActions._operationActions), + concurrent: _concurrent || otherActions.Concurrent); return actions; } + + /// + /// Builder for that uses to accumulate actions + /// and avoid intermediate allocations during construction. + /// On first Append of an array, stores ImmutableArray directly. + /// On subsequent Appends, promotes to ArrayBuilder for efficient merging. + /// + internal sealed class Builder + { + // Holds the ImmutableArray from first non-empty Append + private ImmutableArray _compilationStartActionsImmutable = []; + private ImmutableArray _compilationEndActionsImmutable = []; + private ImmutableArray _compilationActionsImmutable = []; + private ImmutableArray _syntaxTreeActionsImmutable = []; + private ImmutableArray _additionalFileActionsImmutable = []; + private ImmutableArray _semanticModelActionsImmutable = []; + private ImmutableArray _symbolActionsImmutable = []; + private ImmutableArray _symbolStartActionsImmutable = []; + private ImmutableArray _symbolEndActionsImmutable = []; + private ImmutableArray _codeBlockStartActionsImmutable = []; + private ImmutableArray _codeBlockEndActionsImmutable = []; + private ImmutableArray _codeBlockActionsImmutable = []; + private ImmutableArray _operationBlockStartActionsImmutable = []; + private ImmutableArray _operationBlockEndActionsImmutable = []; + private ImmutableArray _operationBlockActionsImmutable = []; + private ImmutableArray _syntaxNodeActionsImmutable = []; + private ImmutableArray _operationActionsImmutable = []; + + // Created only when merging multiple arrays + private ArrayBuilder? _compilationStartActionsBuilder; + private ArrayBuilder? _compilationEndActionsBuilder; + private ArrayBuilder? _compilationActionsBuilder; + private ArrayBuilder? _syntaxTreeActionsBuilder; + private ArrayBuilder? _additionalFileActionsBuilder; + private ArrayBuilder? _semanticModelActionsBuilder; + private ArrayBuilder? _symbolActionsBuilder; + private ArrayBuilder? _symbolStartActionsBuilder; + private ArrayBuilder? _symbolEndActionsBuilder; + private ArrayBuilder? _codeBlockStartActionsBuilder; + private ArrayBuilder? _codeBlockEndActionsBuilder; + private ArrayBuilder? _codeBlockActionsBuilder; + private ArrayBuilder? _operationBlockStartActionsBuilder; + private ArrayBuilder? _operationBlockEndActionsBuilder; + private ArrayBuilder? _operationBlockActionsBuilder; + private ArrayBuilder? _syntaxNodeActionsBuilder; + private ArrayBuilder? _operationActionsBuilder; + + private bool _concurrent; + + /// + /// Appends actions from another instance. + /// + public void Append(in AnalyzerActions otherActions, bool appendSymbolStartAndSymbolEndActions = true) + { + if (otherActions.IsDefault) + { + throw new ArgumentNullException(nameof(otherActions)); + } + + if (otherActions.IsEmpty) + { + return; + } + + _concurrent = _concurrent || otherActions.Concurrent; + + AppendActions(ref _compilationStartActionsImmutable, ref _compilationStartActionsBuilder, otherActions._compilationStartActions); + AppendActions(ref _compilationEndActionsImmutable, ref _compilationEndActionsBuilder, otherActions._compilationEndActions); + AppendActions(ref _compilationActionsImmutable, ref _compilationActionsBuilder, otherActions._compilationActions); + AppendActions(ref _syntaxTreeActionsImmutable, ref _syntaxTreeActionsBuilder, otherActions._syntaxTreeActions); + AppendActions(ref _additionalFileActionsImmutable, ref _additionalFileActionsBuilder, otherActions._additionalFileActions); + AppendActions(ref _semanticModelActionsImmutable, ref _semanticModelActionsBuilder, otherActions._semanticModelActions); + AppendActions(ref _symbolActionsImmutable, ref _symbolActionsBuilder, otherActions._symbolActions); + + if (appendSymbolStartAndSymbolEndActions) + { + AppendActions(ref _symbolStartActionsImmutable, ref _symbolStartActionsBuilder, otherActions._symbolStartActions); + AppendActions(ref _symbolEndActionsImmutable, ref _symbolEndActionsBuilder, otherActions._symbolEndActions); + } + + AppendActions(ref _codeBlockStartActionsImmutable, ref _codeBlockStartActionsBuilder, otherActions._codeBlockStartActions); + AppendActions(ref _codeBlockEndActionsImmutable, ref _codeBlockEndActionsBuilder, otherActions._codeBlockEndActions); + AppendActions(ref _codeBlockActionsImmutable, ref _codeBlockActionsBuilder, otherActions._codeBlockActions); + AppendActions(ref _syntaxNodeActionsImmutable, ref _syntaxNodeActionsBuilder, otherActions._syntaxNodeActions); + AppendActions(ref _operationActionsImmutable, ref _operationActionsBuilder, otherActions._operationActions); + AppendActions(ref _operationBlockStartActionsImmutable, ref _operationBlockStartActionsBuilder, otherActions._operationBlockStartActions); + AppendActions(ref _operationBlockEndActionsImmutable, ref _operationBlockEndActionsBuilder, otherActions._operationBlockEndActions); + AppendActions(ref _operationBlockActionsImmutable, ref _operationBlockActionsBuilder, otherActions._operationBlockActions); + } + + private static void AppendActions( + ref ImmutableArray immutable, + ref ArrayBuilder? builder, + ImmutableArray actions) + { + if (actions.IsDefaultOrEmpty) + { + return; + } + + // First append for this array: just store the immutable array (zero-copy) + if (immutable.IsEmpty) + { + immutable = actions; + return; + } + + // Additional appends for this array: promote to builder + if (builder is null) + { + builder = ArrayBuilder.GetInstance(); + builder.AddRange(immutable); + } + + builder.AddRange(actions); + } + + /// + /// Creates an from the accumulated actions. + /// This method should be called once after all Append operations are complete. + /// + public AnalyzerActions ToAnalyzerActionsAndFree() + { + return new AnalyzerActions( + compilationStartActions: ToImmutableAndFree(ref _compilationStartActionsImmutable, ref _compilationStartActionsBuilder), + compilationEndActions: ToImmutableAndFree(ref _compilationEndActionsImmutable, ref _compilationEndActionsBuilder), + compilationActions: ToImmutableAndFree(ref _compilationActionsImmutable, ref _compilationActionsBuilder), + syntaxTreeActions: ToImmutableAndFree(ref _syntaxTreeActionsImmutable, ref _syntaxTreeActionsBuilder), + additionalFileActions: ToImmutableAndFree(ref _additionalFileActionsImmutable, ref _additionalFileActionsBuilder), + semanticModelActions: ToImmutableAndFree(ref _semanticModelActionsImmutable, ref _semanticModelActionsBuilder), + symbolActions: ToImmutableAndFree(ref _symbolActionsImmutable, ref _symbolActionsBuilder), + symbolStartActions: ToImmutableAndFree(ref _symbolStartActionsImmutable, ref _symbolStartActionsBuilder), + symbolEndActions: ToImmutableAndFree(ref _symbolEndActionsImmutable, ref _symbolEndActionsBuilder), + codeBlockStartActions: ToImmutableAndFree(ref _codeBlockStartActionsImmutable, ref _codeBlockStartActionsBuilder), + codeBlockEndActions: ToImmutableAndFree(ref _codeBlockEndActionsImmutable, ref _codeBlockEndActionsBuilder), + codeBlockActions: ToImmutableAndFree(ref _codeBlockActionsImmutable, ref _codeBlockActionsBuilder), + operationBlockStartActions: ToImmutableAndFree(ref _operationBlockStartActionsImmutable, ref _operationBlockStartActionsBuilder), + operationBlockEndActions: ToImmutableAndFree(ref _operationBlockEndActionsImmutable, ref _operationBlockEndActionsBuilder), + operationBlockActions: ToImmutableAndFree(ref _operationBlockActionsImmutable, ref _operationBlockActionsBuilder), + syntaxNodeActions: ToImmutableAndFree(ref _syntaxNodeActionsImmutable, ref _syntaxNodeActionsBuilder), + operationActions: ToImmutableAndFree(ref _operationActionsImmutable, ref _operationActionsBuilder), + concurrent: _concurrent); + } + + private static ImmutableArray ToImmutableAndFree( + ref ImmutableArray immutable, + ref ArrayBuilder? builder) + { + var result = builder is not null + ? builder.ToImmutableAndFree() + : immutable; + + builder = null; + immutable = default; + + return result; + } + } } } diff --git a/src/Compilers/Core/Portable/EncodedStringText.cs b/src/Compilers/Core/Portable/EncodedStringText.cs index 61fa6f128e68..799accc75d97 100644 --- a/src/Compilers/Core/Portable/EncodedStringText.cs +++ b/src/Compilers/Core/Portable/EncodedStringText.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Text; +using System.Threading; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Text @@ -13,6 +14,9 @@ internal static class EncodedStringText { private const int LargeObjectHeapLimitInChars = 40 * 1024; // 40KB + // Note: CodePagesEncodingProvider.Instance may be null on .NET Framework. + private static volatile bool s_encodingProviderRegistered = CodePagesEncodingProvider.Instance == null; + /// /// Encoding to use when there is no byte order mark (BOM) on the stream. This encoder may throw a /// if the stream contains invalid UTF-8 bytes. @@ -29,23 +33,64 @@ internal static class EncodedStringText /// internal static Encoding CreateFallbackEncoding() { + // Try to get the default ANSI code page in the operating system's + // regional and language settings, and fall back to 1252 otherwise + return TryGetCodePageEncoding(0) + ?? TryGetCodePageEncoding(1252) + ?? Encoding.GetEncoding(name: "Latin1"); + } + + internal static Encoding? TryGetCodePageEncoding(int codePage) + { + // Read to local to avoid race condition when multiple threads fail to get the encoding, + // only one of them executes the filter to retry, sets the static field and suppresses retry for the other threads. + var encodingProviderRegistered = s_encodingProviderRegistered; + try { - if (CodePagesEncodingProvider.Instance != null) + return Encoding.GetEncoding(codePage); + } + catch (NotSupportedException) when (!encodingProviderRegistered) + { + // From documentation: + // - 'GetEncoding' throws NotSupportedException when codepage is not supported by the underlying platform. + // - 'EncodingProvider.Instance' gets an encoding provider for code pages supported + // in the desktop .NET Framework but not by the current underlying platform. + // - 'Encoding.RegisterProvider' makes character encodings available on a platform that does not otherwise support them. + // * Once the encoding provider is registered, the encodings that it supports can be retrieved by calling any + // Encoding.GetEncoding overload. + // * Registering an encoding provider by using the 'RegisterProvider' method also affects the behavior of + // GetEncoding(Int32) when passed an argument of 0. + // * If multiple providers are registered, GetEncoding(Int32) attempts to retrieve the encoding from the most recently + // registered provider first. + // * If the 'RegisterProvider' method is called to register multiple providers that handle the same encoding, + // the last registered provider is the used for all encoding and decoding operations. Any previously registered providers are ignored. + // * If the same encoding provider is used in multiple calls to the 'RegisterProvider' method, + // only the first method call registers the provider. Subsequent calls are ignored. + // + // Given all that: + // - We don't call 'Encoding.RegisterProvider' unconditionally to avoid changing environment + // that is already configured to support the requested codepage. We call it only when we encounter + // a 'NotSupportedException'. + // - We also do not attempt to call 'Encoding.RegisterProvider' more than once. + try { - // If we're running on CoreCLR we have to register the CodePagesEncodingProvider - // first + // Ignore any exceptions from an attempt to register the provider. Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } + catch + { + } + + s_encodingProviderRegistered = true; - // Try to get the default ANSI code page in the operating system's - // regional and language settings, and fall back to 1252 otherwise - return Encoding.GetEncoding(0) - ?? Encoding.GetEncoding(1252); + // Try to get the encoding again after attempting to register the provider. + // Since we set `s_registeredEncodingProvider` to true, we won't get here again. + return TryGetCodePageEncoding(codePage); } - catch (NotSupportedException) + catch (Exception) { - return Encoding.GetEncoding(name: "Latin1"); + return null; } } diff --git a/src/Compilers/Core/Portable/Generated/FlowAnalysis.Generated.cs b/src/Compilers/Core/Portable/Generated/FlowAnalysis.Generated.cs index 58caf7901843..7ec409e28278 100644 --- a/src/Compilers/Core/Portable/Generated/FlowAnalysis.Generated.cs +++ b/src/Compilers/Core/Portable/Generated/FlowAnalysis.Generated.cs @@ -5,6 +5,7 @@ #nullable enable using System; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.PooledObjects; diff --git a/src/Compilers/Core/Portable/Generated/OperationKind.Generated.cs b/src/Compilers/Core/Portable/Generated/OperationKind.Generated.cs index c920786f9d2d..08e8f14e0119 100644 --- a/src/Compilers/Core/Portable/Generated/OperationKind.Generated.cs +++ b/src/Compilers/Core/Portable/Generated/OperationKind.Generated.cs @@ -5,6 +5,7 @@ #nullable enable using System; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.FlowAnalysis; using Microsoft.CodeAnalysis.Operations; namespace Microsoft.CodeAnalysis @@ -286,6 +287,7 @@ public enum OperationKind /// Indicates an . Spread = 0x80, /// Indicates an . + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] CollectionExpressionElementsPlaceholder = 0x81, } } diff --git a/src/Compilers/Core/Portable/Generated/Operations.Generated.cs b/src/Compilers/Core/Portable/Generated/Operations.Generated.cs index b5cd48551be0..6f8cc2f38ae7 100644 --- a/src/Compilers/Core/Portable/Generated/Operations.Generated.cs +++ b/src/Compilers/Core/Portable/Generated/Operations.Generated.cs @@ -3968,6 +3968,7 @@ public interface ICollectionExpressionOperation : IOperation /// . The actual elements passed /// to the creation method are contained in . /// + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] ImmutableArray ConstructArguments { get; } /// /// Collection expression elements. @@ -4028,6 +4029,7 @@ public interface ISpreadOperation : IOperation /// This interface is reserved for implementation by its associated APIs. We reserve the right to /// change it in the future. /// + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] public interface ICollectionExpressionElementsPlaceholderOperation : IOperation { } @@ -11620,6 +11622,7 @@ internal virtual void VisitNoneOperation(IOperation operation) { /* no-op */ } public virtual void VisitInlineArrayAccess(IInlineArrayAccessOperation operation) => DefaultVisit(operation); public virtual void VisitCollectionExpression(ICollectionExpressionOperation operation) => DefaultVisit(operation); public virtual void VisitSpread(ISpreadOperation operation) => DefaultVisit(operation); + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] public virtual void VisitCollectionExpressionElementsPlaceholder(ICollectionExpressionElementsPlaceholderOperation operation) => DefaultVisit(operation); } public abstract partial class OperationVisitor @@ -11760,6 +11763,7 @@ public abstract partial class OperationVisitor public virtual TResult? VisitInlineArrayAccess(IInlineArrayAccessOperation operation, TArgument argument) => DefaultVisit(operation, argument); public virtual TResult? VisitCollectionExpression(ICollectionExpressionOperation operation, TArgument argument) => DefaultVisit(operation, argument); public virtual TResult? VisitSpread(ISpreadOperation operation, TArgument argument) => DefaultVisit(operation, argument); + [Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @"https://github.com/dotnet/roslyn/issues/82210")] public virtual TResult? VisitCollectionExpressionElementsPlaceholder(ICollectionExpressionElementsPlaceholderOperation operation, TArgument argument) => DefaultVisit(operation, argument); } #endregion diff --git a/src/Compilers/Core/Portable/InternalUtilities/NoMessagePumpSyncContext.cs b/src/Compilers/Core/Portable/InternalUtilities/NoMessagePumpSyncContext.cs new file mode 100644 index 000000000000..b5ec9f7d9b6e --- /dev/null +++ b/src/Compilers/Core/Portable/InternalUtilities/NoMessagePumpSyncContext.cs @@ -0,0 +1,79 @@ +#pragma warning disable IDE0073 // We are preserving the original copyright header for this file + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// This was copied from https://github.com/microsoft/vs-threading/blob/4332894cbeaad95797e24004cf3adc5abc5b9be7/src/Microsoft.VisualStudio.Threading/NoMessagePumpSyncContext.cs +// with some changes to reintroduce the P/Invoke directly since we're not using CsWin32. + +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Roslyn.Utilities; + +/// +/// A SynchronizationContext whose synchronously blocking Wait method does not allow +/// any reentrancy via the message pump. +/// +internal sealed class NoMessagePumpSyncContext : SynchronizationContext +{ + /// + /// A shared singleton. + /// + private static readonly SynchronizationContext DefaultInstance = new NoMessagePumpSyncContext(); + + /// + /// Initializes a new instance of the class. + /// + public NoMessagePumpSyncContext() + { + // This is required so that our override of Wait is invoked. + this.SetWaitNotificationRequired(); + } + + /// + /// Gets a shared instance of this class. + /// + public static SynchronizationContext Default + { + get { return DefaultInstance; } + } + + /// + /// Synchronously blocks without a message pump. + /// + /// An array of type that contains the native operating system handles. + /// true to wait for all handles; false to wait for any handle. + /// The number of milliseconds to wait, or (-1) to wait indefinitely. + /// + /// The array index of the object that satisfied the wait. + /// + public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) + { + // On .NET Framework we must take special care to NOT end up in a call to CoWait (which lets in RPC calls). + // Off Windows, we can't p/invoke to kernel32, but it appears that .NET Core never calls CoWait, so we can rely on default behavior. + // We're just going to use the OS as the switch instead of the framework so that (one day) if we drop our .NET Framework specific target, + // and if .NET Core ever adds CoWait support on Windows, we'll still behave properly. + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + return (int)WaitForMultipleObjects((uint)waitHandles.Length, waitHandles, waitAll, (uint)millisecondsTimeout); + } + else + { + return WaitHelper(waitHandles, waitAll, millisecondsTimeout); + } + } + + /// + /// Really truly non pumping wait. + /// Raw IntPtrs have to be used, because the marshaller does not support arrays of SafeHandle, only + /// single SafeHandles. + /// + /// The number of handles in the array. + /// The handles to wait for. + /// A flag indicating whether all handles must be signaled before returning. + /// A timeout that will cause this method to return. + [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)] + private static extern int WaitForMultipleObjects(uint handleCount, IntPtr[] waitHandles, [MarshalAs(UnmanagedType.Bool)] bool waitAll, uint millisecondsTimeout); +} diff --git a/src/Compilers/Core/Portable/InternalUtilities/RoslynExperiments.cs b/src/Compilers/Core/Portable/InternalUtilities/RoslynExperiments.cs index 319732e193bb..4b77e6523cfb 100644 --- a/src/Compilers/Core/Portable/InternalUtilities/RoslynExperiments.cs +++ b/src/Compilers/Core/Portable/InternalUtilities/RoslynExperiments.cs @@ -15,6 +15,9 @@ internal static class RoslynExperiments internal const string GeneratorHostOutputs = "RSEXPERIMENTAL004"; internal const string GeneratorHostOutputs_Url = "https://github.com/dotnet/roslyn/issues/74753"; + // The UrlFormat property is customized per-api to point at a public API tracking issue for the feature, not a single general issue. + internal const string PreviewLanguageFeatureApi = "RSEXPERIMENTAL006"; + // Previously taken: RSEXPERIMENTAL003 - https://github.com/dotnet/roslyn/issues/73002 (SyntaxTokenParser) // Previously taken: RSEXPERIMENTAL005 - https://github.com/dotnet/roslyn/issues/77697 } diff --git a/src/Compilers/Core/Portable/InternalUtilities/SemaphoreSlimExtensions.cs b/src/Compilers/Core/Portable/InternalUtilities/SemaphoreSlimExtensions.cs index 27fa928fab65..b3c02c350f9e 100644 --- a/src/Compilers/Core/Portable/InternalUtilities/SemaphoreSlimExtensions.cs +++ b/src/Compilers/Core/Portable/InternalUtilities/SemaphoreSlimExtensions.cs @@ -13,7 +13,14 @@ internal static class SemaphoreSlimExtensions { public static SemaphoreDisposer DisposableWait(this SemaphoreSlim semaphore, CancellationToken cancellationToken = default) { - semaphore.Wait(cancellationToken); + // Disallow pumping while we do this wait; since this wait often happens on the UI thread, in Visual Studio we can end up with reentrancy + // which can cause all sorts of problems. See https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2600824 or + // https://developercommunity.visualstudio.com/t/Visual-Studio-2022-frequently-freezes-at/10053736 for various examples. + using (SpecializedSyncContext.Apply(NoMessagePumpSyncContext.Default)) + { + semaphore.Wait(cancellationToken); + } + return new SemaphoreDisposer(semaphore); } diff --git a/src/Compilers/Core/Portable/InternalUtilities/SpecializedSyncContext.cs b/src/Compilers/Core/Portable/InternalUtilities/SpecializedSyncContext.cs new file mode 100644 index 000000000000..695ad1f99245 --- /dev/null +++ b/src/Compilers/Core/Portable/InternalUtilities/SpecializedSyncContext.cs @@ -0,0 +1,75 @@ +#pragma warning disable IDE0073 // We are preserving the original copyright header for this file + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// This was copied from https://github.com/microsoft/vs-threading/blob/4332894cbeaad95797e24004cf3adc5abc5b9be7/src/Microsoft.VisualStudio.Threading/SpecializedSyncContext.cs with some changes to +// match naming and conventions in Roslyn. + +using System; +using System.Threading; +using Microsoft.CodeAnalysis.ErrorReporting; + +namespace Roslyn.Utilities; + +/// +/// A structure that applies and reverts changes to the . +/// +internal readonly struct SpecializedSyncContext : IDisposable +{ + /// + /// A flag indicating whether the non-default constructor was invoked. + /// + private readonly bool _initialized; + + /// + /// The SynchronizationContext to restore when is invoked. + /// + private readonly SynchronizationContext? _prior; + + /// + /// The SynchronizationContext applied when this struct was constructed. + /// + private readonly SynchronizationContext? _appliedContext; + + /// + /// A value indicating whether to check that the applied SyncContext is still the current one when the original is restored. + /// + private readonly bool _checkForChangesOnRevert; + + /// + /// Initializes a new instance of the struct. + /// + private SpecializedSyncContext(SynchronizationContext? syncContext, bool checkForChangesOnRevert) + { + this._initialized = true; + this._prior = SynchronizationContext.Current; + this._appliedContext = syncContext; + this._checkForChangesOnRevert = checkForChangesOnRevert; + SynchronizationContext.SetSynchronizationContext(syncContext); + } + + /// + /// Applies the specified to the caller's context. + /// + /// The synchronization context to apply. + /// A value indicating whether to check that the applied SyncContext is still the current one when the original is restored. + public static SpecializedSyncContext Apply(SynchronizationContext? syncContext, bool checkForChangesOnRevert = true) + { + return new SpecializedSyncContext(syncContext, checkForChangesOnRevert); + } + + /// + /// Reverts the SynchronizationContext to its previous instance. + /// + public void Dispose() + { + if (this._initialized) + { + if (this._checkForChangesOnRevert && SynchronizationContext.Current != this._appliedContext) + FatalError.ReportNonFatalError(new Exception("The SynchronizationContext was changed since it was applied.")); + + SynchronizationContext.SetSynchronizationContext(this._prior); + } + } +} diff --git a/src/Compilers/Core/Portable/InternalUtilities/ThreadSafeFlagOperations.cs b/src/Compilers/Core/Portable/InternalUtilities/ThreadSafeFlagOperations.cs index 536e48c2caba..af304f9e051e 100644 --- a/src/Compilers/Core/Portable/InternalUtilities/ThreadSafeFlagOperations.cs +++ b/src/Compilers/Core/Portable/InternalUtilities/ThreadSafeFlagOperations.cs @@ -24,6 +24,22 @@ public static bool Set(ref int flags, int toSet) return true; } + public static bool Set(ref long flags, long toSet) + { + long oldState, newState; + do + { + oldState = flags; + newState = oldState | toSet; + if (newState == oldState) + { + return false; + } + } + while (Interlocked.CompareExchange(ref flags, newState, oldState) != oldState); + return true; + } + public static bool Clear(ref int flags, int toClear) { int oldState, newState; diff --git a/src/Compilers/Core/Portable/InternalUtilities/UICultureUtilities.cs b/src/Compilers/Core/Portable/InternalUtilities/UICultureUtilities.cs index 43db5ec89535..f820697bc6a2 100644 --- a/src/Compilers/Core/Portable/InternalUtilities/UICultureUtilities.cs +++ b/src/Compilers/Core/Portable/InternalUtilities/UICultureUtilities.cs @@ -3,135 +3,28 @@ // See the LICENSE file in the project root for more information. using System; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Reflection; namespace Roslyn.Utilities { internal static class UICultureUtilities { - // TODO (DevDiv 1117307): Replace with CultureInfo.CurrentUICulture.set when available. - private const string currentUICultureName = "CurrentUICulture"; - private static readonly Action? s_setCurrentUICulture; - - private static bool TryGetCurrentUICultureSetter([NotNullWhen(returnValue: true)] out Action? setter) - { - const string cultureInfoTypeName = "System.Globalization.CultureInfo"; - const string cultureInfoTypeNameGlobalization = cultureInfoTypeName + ", System.Globalization, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"; - - try - { - var type = Type.GetType(cultureInfoTypeNameGlobalization) ?? typeof(object).GetTypeInfo().Assembly.GetType(cultureInfoTypeName); - if ((object?)type == null) - { - setter = null; - return false; - } - - var currentUICultureSetter = type.GetTypeInfo().GetDeclaredProperty(currentUICultureName)?.SetMethod; - if ((object?)currentUICultureSetter == null || !currentUICultureSetter.IsStatic || currentUICultureSetter.ContainsGenericParameters || currentUICultureSetter.ReturnType != typeof(void)) - { - setter = null; - return false; - } - - var parameters = currentUICultureSetter.GetParameters(); - if (parameters.Length != 1 || parameters[0].ParameterType != typeof(CultureInfo)) - { - setter = null; - return false; - } - - setter = (Action)currentUICultureSetter.CreateDelegate(typeof(Action)); - return true; - } - catch - { - setter = null; - return false; - } - } - - private static bool TryGetCurrentThreadUICultureSetter([NotNullWhen(returnValue: true)] out Action? setter) - { - const string threadTypeName = "System.Threading.Thread"; - const string currentThreadName = "CurrentThread"; - - try - { - var type = typeof(object).GetTypeInfo().Assembly.GetType(threadTypeName); - if (type is null) - { - setter = null; - return false; - } - - var typeInfo = type.GetTypeInfo(); - var currentThreadGetter = typeInfo.GetDeclaredProperty(currentThreadName)?.GetMethod; - if ((object?)currentThreadGetter == null || !currentThreadGetter.IsStatic || currentThreadGetter.ContainsGenericParameters || currentThreadGetter.ReturnType != type || currentThreadGetter.GetParameters().Length != 0) - { - setter = null; - return false; - } - - var currentUICultureSetter = typeInfo.GetDeclaredProperty(currentUICultureName)?.SetMethod; - if ((object?)currentUICultureSetter == null || currentUICultureSetter.IsStatic || currentUICultureSetter.ContainsGenericParameters || currentUICultureSetter.ReturnType != typeof(void)) - { - setter = null; - return false; - } - - var parameters = currentUICultureSetter.GetParameters(); - if (parameters.Length != 1 || parameters[0].ParameterType != typeof(CultureInfo)) - { - setter = null; - return false; - } - - setter = culture => - { - currentUICultureSetter.Invoke(currentThreadGetter.Invoke(null, null), new[] { culture }); - }; - return true; - } - catch - { - setter = null; - return false; - } - } - - static UICultureUtilities() - { - if (!TryGetCurrentUICultureSetter(out s_setCurrentUICulture) && - !TryGetCurrentThreadUICultureSetter(out s_setCurrentUICulture)) - { - s_setCurrentUICulture = null; - } - } - public static Action WithCurrentUICulture(Action action) { - if (s_setCurrentUICulture == null) - { - return action; - } - var savedCulture = CultureInfo.CurrentUICulture; return () => { var currentCulture = CultureInfo.CurrentUICulture; if (currentCulture != savedCulture) { - s_setCurrentUICulture(savedCulture); + CultureInfo.CurrentUICulture = savedCulture; try { action(); } finally { - s_setCurrentUICulture(currentCulture); + CultureInfo.CurrentUICulture = currentCulture; } } else @@ -143,25 +36,20 @@ public static Action WithCurrentUICulture(Action action) public static Action WithCurrentUICulture(Action action) { - if (s_setCurrentUICulture == null) - { - return action; - } - var savedCulture = CultureInfo.CurrentUICulture; return param => { var currentCulture = CultureInfo.CurrentUICulture; if (currentCulture != savedCulture) { - s_setCurrentUICulture(savedCulture); + CultureInfo.CurrentUICulture = savedCulture; try { action(param); } finally { - s_setCurrentUICulture(currentCulture); + CultureInfo.CurrentUICulture = currentCulture; } } else @@ -173,25 +61,20 @@ public static Action WithCurrentUICulture(Action action) public static Func WithCurrentUICulture(Func func) { - if (s_setCurrentUICulture == null) - { - return func; - } - var savedCulture = CultureInfo.CurrentUICulture; return () => { var currentCulture = CultureInfo.CurrentUICulture; if (currentCulture != savedCulture) { - s_setCurrentUICulture(savedCulture); + CultureInfo.CurrentUICulture = savedCulture; try { return func(); } finally { - s_setCurrentUICulture(currentCulture); + CultureInfo.CurrentUICulture = currentCulture; } } else diff --git a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs index 3decb73f99d2..838cf0ee8377 100644 --- a/src/Compilers/Core/Portable/MetadataReader/PEModule.cs +++ b/src/Compilers/Core/Portable/MetadataReader/PEModule.cs @@ -1184,6 +1184,23 @@ internal bool HasRefSafetyRulesAttribute(EntityHandle token, out int version, ou return false; } + internal bool HasMemorySafetyRulesAttribute(EntityHandle token, out int version, out bool foundAttributeType) + { + AttributeInfo info = FindTargetAttribute(MetadataReader, token, AttributeDescription.MemorySafetyRulesAttribute, out foundAttributeType); + if (info.HasValue) + { + Debug.Assert(info.SignatureIndex == 0); + if (TryExtractValueFromAttribute(info.Handle, out int value, s_attributeIntValueExtractor)) + { + version = value; + return true; + } + } + + version = 0; + return false; + } + internal bool HasInlineArrayAttribute(TypeDefinitionHandle token, out int length) { AttributeInfo info = FindTargetAttribute(token, AttributeDescription.InlineArrayAttribute); diff --git a/src/Compilers/Core/Portable/Operations/CommonConversion.cs b/src/Compilers/Core/Portable/Operations/CommonConversion.cs index 46303fd60a55..9f8a39275e33 100644 --- a/src/Compilers/Core/Portable/Operations/CommonConversion.cs +++ b/src/Compilers/Core/Portable/Operations/CommonConversion.cs @@ -73,9 +73,23 @@ internal CommonConversion(bool exists, bool isIdentity, bool isNumeric, bool isR /// Returns true if the conversion is a user-defined conversion. /// [MemberNotNullWhen(true, nameof(MethodSymbol))] - public bool IsUserDefined => MethodSymbol != null; + public bool IsUserDefined => MethodSymbol is { MethodKind: MethodKind.Conversion }; /// - /// Returns the method used to perform the conversion for a user-defined conversion if is true. + /// Returns true if the conversion is a union conversion. + /// + [MemberNotNullWhen(true, nameof(MethodSymbol))] + public bool IsUnion + { + [Experimental(RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = "https://github.com/dotnet/roslyn/issues/82567")] + get + { + return MethodSymbol is { MethodKind: MethodKind.Constructor }; + } + } + + /// + /// Returns the method used to perform the conversion for a user-defined conversion if is true, + /// or to perform the conversion for union conversion if is true. /// Otherwise, returns null. /// public IMethodSymbol? MethodSymbol { get; } diff --git a/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.InterpolatedStringContext.cs b/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.InterpolatedStringContext.cs index 7136476fc2de..7e83b7ad3ba3 100644 --- a/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.InterpolatedStringContext.cs +++ b/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.InterpolatedStringContext.cs @@ -58,10 +58,21 @@ private void AssertContainingContextIsForThisCreation(IOperation placeholderOper Debug.Assert(operation != null); Debug.Assert(_currentInterpolatedStringHandlerCreationContext.ApplicableCreationOperation == operation); + // Note: _currentInterpolatedStringHandlerArgumentContext may be null in error scenarios + // (for example, indexer with no setter in object initializer where the handler creation is visited + // as a child of an InvalidOperation rather than through VisitAndPushArguments). + // If a new test triggers this assert because a different ancestor has the error, it will likely be fine to just add + // that case to the assert. if (assertArgumentContext) { - Debug.Assert(_currentInterpolatedStringHandlerArgumentContext != null); - Debug.Assert(_currentInterpolatedStringHandlerArgumentContext.ApplicableCreationOperations.Contains((IInterpolatedStringHandlerCreationOperation)operation)); + if (_currentInterpolatedStringHandlerArgumentContext != null) + { + Debug.Assert(_currentInterpolatedStringHandlerArgumentContext.ApplicableCreationOperations.Contains((IInterpolatedStringHandlerCreationOperation)operation)); + } + else + { + Debug.Assert(placeholderOperation.Parent!.Parent is IObjectCreationOperation objectCreation && objectCreation.HasErrors(_compilation)); + } } } } diff --git a/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.cs b/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.cs index 9ed8d1815fbc..8697889a071b 100644 --- a/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.cs +++ b/src/Compilers/Core/Portable/Operations/ControlFlowGraphBuilder.cs @@ -6996,8 +6996,10 @@ static void pushLeftNodes(IInterpolatedStringAdditionOperation addition, ArrayBu case InterpolatedStringArgumentPlaceholderKind.CallsiteReceiver: AssertContainingContextIsForThisCreation(operation, assertArgumentContext: true); - Debug.Assert(_currentInterpolatedStringHandlerArgumentContext != null); - if (_currentInterpolatedStringHandlerArgumentContext.HasReceiver && tryGetArgumentOrReceiver(-1) is IOperation receiverCapture) + // Context may be null in error scenarios (for example, indexer with no setter in object initializer) + if (_currentInterpolatedStringHandlerArgumentContext != null && + _currentInterpolatedStringHandlerArgumentContext.HasReceiver && + tryGetArgumentOrReceiver(-1) is IOperation receiverCapture) { Debug.Assert(receiverCapture is IFlowCaptureReferenceOperation); return OperationCloner.CloneOperation(receiverCapture); @@ -7009,8 +7011,9 @@ static void pushLeftNodes(IInterpolatedStringAdditionOperation addition, ArrayBu case InterpolatedStringArgumentPlaceholderKind.CallsiteArgument: AssertContainingContextIsForThisCreation(operation, assertArgumentContext: true); - Debug.Assert(_currentInterpolatedStringHandlerArgumentContext != null); - if (tryGetArgumentOrReceiver(operation.ArgumentIndex) is IOperation argumentCapture) + // Context may be null in error scenarios (for example, indexer with no setter in object initializer) + if (_currentInterpolatedStringHandlerArgumentContext != null && + tryGetArgumentOrReceiver(operation.ArgumentIndex) is IOperation argumentCapture) { Debug.Assert(argumentCapture is IFlowCaptureReferenceOperation or IDiscardOperation); return OperationCloner.CloneOperation(argumentCapture); diff --git a/src/Compilers/Core/Portable/Operations/OperationInterfaces.xml b/src/Compilers/Core/Portable/Operations/OperationInterfaces.xml index 325f35130bd4..e6439f9758e1 100644 --- a/src/Compilers/Core/Portable/Operations/OperationInterfaces.xml +++ b/src/Compilers/Core/Portable/Operations/OperationInterfaces.xml @@ -3697,7 +3697,7 @@ - + Arguments passed to to , if present. Arguments are in evaluation order. This can @@ -3761,7 +3761,7 @@ - + Represents the elements of a collection expression as they are passed to some construction method diff --git a/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs b/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs index a42ba4ff3d01..249b25689b48 100644 --- a/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs +++ b/src/Compilers/Core/Portable/PEWriter/MetadataWriter.cs @@ -3461,14 +3461,9 @@ private void SerializeFieldSignature(IFieldReference fieldReference, BlobBuilder { Debug.Assert(fieldReference.RefCustomModifiers.Length == 0 || fieldReference.IsByReference); - // https://github.com/dotnet/roslyn/issues/61385: Use System.Reflection.Metadata.Ecma335.FieldTypeEncoder - // instead, since that type supports ref fields directly. - var typeEncoder = new BlobEncoder(builder).FieldSignature(); - SerializeCustomModifiers(new CustomModifiersEncoder(builder), fieldReference.RefCustomModifiers); - if (fieldReference.IsByReference) - { - typeEncoder.Builder.WriteByte((byte)SignatureTypeCode.ByReference); - } + var fieldTypeEncoder = new BlobEncoder(builder).Field(); + SerializeCustomModifiers(fieldTypeEncoder.CustomModifiers(), fieldReference.RefCustomModifiers); + var typeEncoder = fieldTypeEncoder.Type(fieldReference.IsByReference); SerializeTypeReference(typeEncoder, fieldReference.GetType(Context)); } diff --git a/src/Compilers/Core/Portable/PublicAPI.Shipped.txt b/src/Compilers/Core/Portable/PublicAPI.Shipped.txt index 4c39dc8fa52f..5345d12e6201 100644 --- a/src/Compilers/Core/Portable/PublicAPI.Shipped.txt +++ b/src/Compilers/Core/Portable/PublicAPI.Shipped.txt @@ -161,7 +161,7 @@ abstract Microsoft.CodeAnalysis.SemanticModel.LookupLabelsCore(int position, str abstract Microsoft.CodeAnalysis.SemanticModel.LookupNamespacesAndTypesCore(int position, Microsoft.CodeAnalysis.INamespaceOrTypeSymbol? container, string? name) -> System.Collections.Immutable.ImmutableArray abstract Microsoft.CodeAnalysis.SemanticModel.LookupStaticMembersCore(int position, Microsoft.CodeAnalysis.INamespaceOrTypeSymbol? container, string? name) -> System.Collections.Immutable.ImmutableArray abstract Microsoft.CodeAnalysis.SemanticModel.LookupSymbolsCore(int position, Microsoft.CodeAnalysis.INamespaceOrTypeSymbol? container, string? name, bool includeReducedExtensionMethods) -> System.Collections.Immutable.ImmutableArray -abstract Microsoft.CodeAnalysis.SemanticModel.NullableAnalysisIsDisabled.get -> bool +[RSEXPERIMENTAL001]abstract Microsoft.CodeAnalysis.SemanticModel.NullableAnalysisIsDisabled.get -> bool abstract Microsoft.CodeAnalysis.SemanticModel.OriginalPositionForSpeculation.get -> int abstract Microsoft.CodeAnalysis.SemanticModel.ParentModelCore.get -> Microsoft.CodeAnalysis.SemanticModel? abstract Microsoft.CodeAnalysis.SemanticModel.RootCore.get -> Microsoft.CodeAnalysis.SyntaxNode! @@ -1039,6 +1039,8 @@ Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions.EmitDifferenceOptions() -> void Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions.EmitFieldRva.get -> bool Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions.EmitFieldRva.init -> void +Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions.MethodImplEntriesSupported.get -> bool +Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions.MethodImplEntriesSupported.init -> void Microsoft.CodeAnalysis.Emit.EmitDifferenceResult Microsoft.CodeAnalysis.Emit.EmitDifferenceResult.Baseline.get -> Microsoft.CodeAnalysis.Emit.EmitBaseline? Microsoft.CodeAnalysis.Emit.EmitDifferenceResult.ChangedTypes.get -> System.Collections.Immutable.ImmutableArray @@ -1298,7 +1300,7 @@ Microsoft.CodeAnalysis.GeneratorRunResult.Exception.get -> System.Exception? Microsoft.CodeAnalysis.GeneratorRunResult.GeneratedSources.get -> System.Collections.Immutable.ImmutableArray Microsoft.CodeAnalysis.GeneratorRunResult.Generator.get -> Microsoft.CodeAnalysis.ISourceGenerator! Microsoft.CodeAnalysis.GeneratorRunResult.GeneratorRunResult() -> void -Microsoft.CodeAnalysis.GeneratorRunResult.HostOutputs.get -> System.Collections.Immutable.ImmutableDictionary! +[RSEXPERIMENTAL004]Microsoft.CodeAnalysis.GeneratorRunResult.HostOutputs.get -> System.Collections.Immutable.ImmutableDictionary! Microsoft.CodeAnalysis.GeneratorRunResult.TrackedOutputSteps.get -> System.Collections.Immutable.ImmutableDictionary>! Microsoft.CodeAnalysis.GeneratorRunResult.TrackedSteps.get -> System.Collections.Immutable.ImmutableDictionary>! Microsoft.CodeAnalysis.GeneratorSyntaxContext @@ -1432,6 +1434,7 @@ Microsoft.CodeAnalysis.IMethodSymbol.PartialImplementationPart.get -> Microsoft. Microsoft.CodeAnalysis.IMethodSymbol.ReceiverNullableAnnotation.get -> Microsoft.CodeAnalysis.NullableAnnotation Microsoft.CodeAnalysis.IMethodSymbol.ReceiverType.get -> Microsoft.CodeAnalysis.ITypeSymbol? Microsoft.CodeAnalysis.IMethodSymbol.ReducedFrom.get -> Microsoft.CodeAnalysis.IMethodSymbol? +Microsoft.CodeAnalysis.IMethodSymbol.ReduceExtensionMember(Microsoft.CodeAnalysis.ITypeSymbol! receiverType) -> Microsoft.CodeAnalysis.IMethodSymbol? Microsoft.CodeAnalysis.IMethodSymbol.ReduceExtensionMethod(Microsoft.CodeAnalysis.ITypeSymbol! receiverType) -> Microsoft.CodeAnalysis.IMethodSymbol? Microsoft.CodeAnalysis.IMethodSymbol.RefCustomModifiers.get -> System.Collections.Immutable.ImmutableArray Microsoft.CodeAnalysis.IMethodSymbol.RefKind.get -> Microsoft.CodeAnalysis.RefKind @@ -1622,6 +1625,7 @@ Microsoft.CodeAnalysis.IPropertySymbol.OverriddenProperty.get -> Microsoft.CodeA Microsoft.CodeAnalysis.IPropertySymbol.Parameters.get -> System.Collections.Immutable.ImmutableArray Microsoft.CodeAnalysis.IPropertySymbol.PartialDefinitionPart.get -> Microsoft.CodeAnalysis.IPropertySymbol? Microsoft.CodeAnalysis.IPropertySymbol.PartialImplementationPart.get -> Microsoft.CodeAnalysis.IPropertySymbol? +Microsoft.CodeAnalysis.IPropertySymbol.ReduceExtensionMember(Microsoft.CodeAnalysis.ITypeSymbol! receiverType) -> Microsoft.CodeAnalysis.IPropertySymbol? Microsoft.CodeAnalysis.IPropertySymbol.RefCustomModifiers.get -> System.Collections.Immutable.ImmutableArray Microsoft.CodeAnalysis.IPropertySymbol.RefKind.get -> Microsoft.CodeAnalysis.RefKind Microsoft.CodeAnalysis.IPropertySymbol.ReturnsByRef.get -> bool diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index 01c5dbd2a5f3..7bed4716ba5c 100644 --- a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt @@ -1,9 +1,11 @@ -Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions.MethodImplEntriesSupported.get -> bool -Microsoft.CodeAnalysis.Emit.EmitDifferenceOptions.MethodImplEntriesSupported.init -> void -Microsoft.CodeAnalysis.IMethodSymbol.ReduceExtensionMember(Microsoft.CodeAnalysis.ITypeSymbol! receiverType) -> Microsoft.CodeAnalysis.IMethodSymbol? -Microsoft.CodeAnalysis.IPropertySymbol.ReduceExtensionMember(Microsoft.CodeAnalysis.ITypeSymbol! receiverType) -> Microsoft.CodeAnalysis.IPropertySymbol? -Microsoft.CodeAnalysis.OperationKind.CollectionExpressionElementsPlaceholder = 129 -> Microsoft.CodeAnalysis.OperationKind -Microsoft.CodeAnalysis.Operations.ICollectionExpressionElementsPlaceholderOperation -Microsoft.CodeAnalysis.Operations.ICollectionExpressionOperation.ConstructArguments.get -> System.Collections.Immutable.ImmutableArray -virtual Microsoft.CodeAnalysis.Operations.OperationVisitor.VisitCollectionExpressionElementsPlaceholder(Microsoft.CodeAnalysis.Operations.ICollectionExpressionElementsPlaceholderOperation! operation) -> void -virtual Microsoft.CodeAnalysis.Operations.OperationVisitor.VisitCollectionExpressionElementsPlaceholder(Microsoft.CodeAnalysis.Operations.ICollectionExpressionElementsPlaceholderOperation! operation, TArgument argument) -> TResult? +Microsoft.CodeAnalysis.Text.SourceHashAlgorithm.Sha384 = 3 -> Microsoft.CodeAnalysis.Text.SourceHashAlgorithm +Microsoft.CodeAnalysis.Text.SourceHashAlgorithm.Sha512 = 4 -> Microsoft.CodeAnalysis.Text.SourceHashAlgorithm +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.OperationKind.CollectionExpressionElementsPlaceholder = 129 -> Microsoft.CodeAnalysis.OperationKind +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.Operations.ICollectionExpressionOperation.ConstructArguments.get -> System.Collections.Immutable.ImmutableArray +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.Operations.ICollectionExpressionElementsPlaceholderOperation +[RSEXPERIMENTAL006]virtual Microsoft.CodeAnalysis.Operations.OperationVisitor.VisitCollectionExpressionElementsPlaceholder(Microsoft.CodeAnalysis.Operations.ICollectionExpressionElementsPlaceholderOperation! operation) -> void +[RSEXPERIMENTAL006]virtual Microsoft.CodeAnalysis.Operations.OperationVisitor.VisitCollectionExpressionElementsPlaceholder(Microsoft.CodeAnalysis.Operations.ICollectionExpressionElementsPlaceholderOperation! operation, TArgument argument) -> TResult? + +[RSEXPERIMENTAL006]const Microsoft.CodeAnalysis.WellKnownMemberNames.HasValuePropertyName = "HasValue" -> string! +[RSEXPERIMENTAL006]const Microsoft.CodeAnalysis.WellKnownMemberNames.TryGetValueMethodName = "TryGetValue" -> string! +[RSEXPERIMENTAL006]Microsoft.CodeAnalysis.Operations.CommonConversion.IsUnion.get -> bool diff --git a/src/Compilers/Core/Portable/SourceGeneration/GeneratorContexts.cs b/src/Compilers/Core/Portable/SourceGeneration/GeneratorContexts.cs index 23306576e389..ea809fed907a 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/GeneratorContexts.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/GeneratorContexts.cs @@ -92,7 +92,7 @@ internal GeneratorExecutionContext(Compilation compilation, ParseOptions parseOp /// /// Directory separators "/" and "\" are allowed in , they are normalized to "/" regardless of host platform. /// - public void AddSource(string hintName, SourceText sourceText) => _additionalSources.Add(hintName, sourceText.WithChecksumAlgorithm(_checksumAlgorithm)); + public void AddSource(string hintName, SourceText sourceText) => _additionalSources.Add(hintName, sourceText.WithChecksumAlgorithmIfAny(_checksumAlgorithm)); /// /// Adds a to the users compilation diff --git a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs index 2f4ca8f8c605..1da3d4e378f5 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/GeneratorDriver.cs @@ -427,6 +427,15 @@ private static ImmutableArray FilterDiagnostics(Compilation compilat ArrayBuilder filteredDiagnostics = ArrayBuilder.GetInstance(); foreach (var diag in generatorDiagnostics) { + try + { + DiagnosticAnalysisContextHelpers.VerifyArguments(diag, compilation, isSupportedDiagnostic: static (_, _) => true, cancellationToken); + } + catch (ArgumentException ex) + { + throw new UserFunctionException(ex); + } + if (compilation.Options.FilterDiagnostic(diag, cancellationToken) is { } filtered && suppressMessageState.ApplySourceSuppressions(filtered) is { } effective) { diff --git a/src/Compilers/Core/Portable/SourceGeneration/IncrementalContexts.cs b/src/Compilers/Core/Portable/SourceGeneration/IncrementalContexts.cs index 0999fae937ed..6606f0b63ae5 100644 --- a/src/Compilers/Core/Portable/SourceGeneration/IncrementalContexts.cs +++ b/src/Compilers/Core/Portable/SourceGeneration/IncrementalContexts.cs @@ -261,7 +261,7 @@ internal SourceProductionContext(AdditionalSourcesCollection sources, Diagnostic /// /// Directory separators "/" and "\" are allowed in , they are normalized to "/" regardless of host platform. /// - public void AddSource(string hintName, SourceText sourceText) => Sources.Add(hintName, sourceText.WithChecksumAlgorithm(ChecksumAlgorithm)); + public void AddSource(string hintName, SourceText sourceText) => Sources.Add(hintName, sourceText.WithChecksumAlgorithmIfAny(ChecksumAlgorithm)); /// /// Adds a to the users compilation diff --git a/src/Compilers/Core/Portable/SpecialMember.cs b/src/Compilers/Core/Portable/SpecialMember.cs index ed520f7fb22d..ab3ae5d20210 100644 --- a/src/Compilers/Core/Portable/SpecialMember.cs +++ b/src/Compilers/Core/Portable/SpecialMember.cs @@ -29,7 +29,8 @@ internal enum SpecialMember System_String__Chars, System_String__Format, System_String__Format_IFormatProvider, - System_String__Substring, + System_String__SubstringIntInt, + System_String__SubstringInt, System_String__op_Implicit_ToReadOnlySpanOfChar, diff --git a/src/Compilers/Core/Portable/SpecialMembers.cs b/src/Compilers/Core/Portable/SpecialMembers.cs index 2b0f50dab0bd..081d157a7acd 100644 --- a/src/Compilers/Core/Portable/SpecialMembers.cs +++ b/src/Compilers/Core/Portable/SpecialMembers.cs @@ -197,7 +197,7 @@ static SpecialMembers() (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_String, (byte)SignatureTypeCode.SZArray, (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Object, - // System_String__Substring + // System_String__SubstringIntInt (byte)MemberFlags.Method, // Flags (byte)SpecialType.System_String, // DeclaringTypeId 0, // Arity @@ -206,6 +206,14 @@ static SpecialMembers() (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + // System_String__SubstringInt + (byte)MemberFlags.Method, // Flags + (byte)SpecialType.System_String, // DeclaringTypeId + 0, // Arity + 1, // Method Signature + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_String, // Return Type + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + // System_String__op_Implicit_ToReadOnlySpanOfChar (byte)(MemberFlags.Method | MemberFlags.Static), // Flags (byte)SpecialType.System_String, // DeclaringTypeId @@ -1379,7 +1387,8 @@ static SpecialMembers() "get_Chars", // System_String__Chars "Format", // System_String__Format "Format", // System_String__Format_IFormatProvider - "Substring", // System_String__Substring + "Substring", // System_String__SubstringIntInt + "Substring", // System_String__SubstringInt "op_Implicit", // System_String__op_Implicit_ToReadOnlySpanOfChar "IsNaN", // System_Double__IsNaN "IsNaN", // System_Single__IsNaN diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs index 335b68d14c2d..2c670664cd11 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/AttributeDescription.cs @@ -484,6 +484,8 @@ static AttributeDescription() internal static readonly AttributeDescription NativeIntegerAttribute = new AttributeDescription("System.Runtime.CompilerServices", "NativeIntegerAttribute", s_signaturesOfNativeIntegerAttribute); internal static readonly AttributeDescription ScopedRefAttribute = new AttributeDescription("System.Runtime.CompilerServices", "ScopedRefAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription RefSafetyRulesAttribute = new AttributeDescription("System.Runtime.CompilerServices", "RefSafetyRulesAttribute", s_signatures_HasThis_Void_Int32_Only); + internal static readonly AttributeDescription MemorySafetyRulesAttribute = new AttributeDescription("System.Runtime.CompilerServices", "MemorySafetyRulesAttribute", s_signatures_HasThis_Void_Int32_Only); + internal static readonly AttributeDescription RequiresUnsafeAttribute = new AttributeDescription("System.Diagnostics.CodeAnalysis", "RequiresUnsafeAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription ModuleInitializerAttribute = new AttributeDescription("System.Runtime.CompilerServices", "ModuleInitializerAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription UnmanagedCallersOnlyAttribute = new AttributeDescription("System.Runtime.InteropServices", "UnmanagedCallersOnlyAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription InterpolatedStringHandlerAttribute = new AttributeDescription("System.Runtime.CompilerServices", "InterpolatedStringHandlerAttribute", s_signatures_HasThis_Void_Only); @@ -500,6 +502,7 @@ static AttributeDescription() internal static readonly AttributeDescription RuntimeAsyncMethodGenerationAttribute = new AttributeDescription("System.Runtime.CompilerServices", "RuntimeAsyncMethodGenerationAttribute", s_signatures_HasThis_Void_Boolean_Only); internal static readonly AttributeDescription MetadataUpdateDeletedAttribute = new AttributeDescription("System.Runtime.CompilerServices", "MetadataUpdateDeletedAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription ExtendedLayoutAttribute = new AttributeDescription("System.Runtime.InteropServices", "ExtendedLayoutAttribute", s_signaturesOfExtendedLayoutAttribute); + internal static readonly AttributeDescription UnionAttribute = new AttributeDescription("System.Runtime.CompilerServices", "UnionAttribute", s_signatures_HasThis_Void_Only); internal static readonly AttributeDescription ClosedAttribute = new AttributeDescription("System.Runtime.CompilerServices", "ClosedAttribute", s_signatures_HasThis_Void_Only); } } diff --git a/src/Compilers/Core/Portable/Symbols/Attributes/CommonEventWellKnownAttributeData.cs b/src/Compilers/Core/Portable/Symbols/Attributes/CommonEventWellKnownAttributeData.cs index af49e37b20e7..d98c374a7918 100644 --- a/src/Compilers/Core/Portable/Symbols/Attributes/CommonEventWellKnownAttributeData.cs +++ b/src/Compilers/Core/Portable/Symbols/Attributes/CommonEventWellKnownAttributeData.cs @@ -11,6 +11,22 @@ namespace Microsoft.CodeAnalysis /// internal class CommonEventWellKnownAttributeData : WellKnownAttributeData, ISkipLocalsInitAttributeTarget { + private bool _hasRequiresUnsafeAttribute; + public bool HasRequiresUnsafeAttribute + { + get + { + VerifySealed(expected: true); + return _hasRequiresUnsafeAttribute; + } + set + { + VerifySealed(expected: false); + _hasRequiresUnsafeAttribute = value; + SetDataStored(); + } + } + private bool _hasSpecialNameAttribute; public bool HasSpecialNameAttribute { diff --git a/src/Compilers/Core/Portable/Symbols/IAssemblySymbol.cs b/src/Compilers/Core/Portable/Symbols/IAssemblySymbol.cs index ce4452d88025..f59808eec827 100644 --- a/src/Compilers/Core/Portable/Symbols/IAssemblySymbol.cs +++ b/src/Compilers/Core/Portable/Symbols/IAssemblySymbol.cs @@ -63,8 +63,8 @@ public interface IAssemblySymbol : ISymbol INamedTypeSymbol? GetTypeByMetadataName(string fullyQualifiedMetadataName); /// - /// Determines if the assembly might contain extension methods. - /// If false, the assembly does not contain extension methods. + /// Determines if the assembly might contain extension members or methods. + /// If false, the assembly does not contain extension members or methods. /// bool MightContainExtensionMethods { get; } diff --git a/src/Compilers/Core/Portable/Symbols/INamedTypeSymbol.cs b/src/Compilers/Core/Portable/Symbols/INamedTypeSymbol.cs index a3fc06377300..1d3a8aca1fd2 100644 --- a/src/Compilers/Core/Portable/Symbols/INamedTypeSymbol.cs +++ b/src/Compilers/Core/Portable/Symbols/INamedTypeSymbol.cs @@ -165,8 +165,8 @@ public interface INamedTypeSymbol : ITypeSymbol ISymbol? AssociatedSymbol { get; } /// - /// Determines if the symbol might contain extension methods. - /// If false, the symbol does not contain extension methods. + /// Determines if the symbol might contain extension members or methods. + /// If false, the symbol does not contain extension members or methods. /// bool MightContainExtensionMethods { get; } diff --git a/src/Compilers/Core/Portable/Symbols/ITypeSymbol.cs b/src/Compilers/Core/Portable/Symbols/ITypeSymbol.cs index e51c27f69b69..3d9563874554 100644 --- a/src/Compilers/Core/Portable/Symbols/ITypeSymbol.cs +++ b/src/Compilers/Core/Portable/Symbols/ITypeSymbol.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -80,10 +81,12 @@ public interface ITypeSymbol : INamespaceOrTypeSymbol /// bool IsNativeIntegerType { get; } - [Obsolete($"This API will be removed in the future. Use {nameof(INamedTypeSymbol)}.{nameof(INamedTypeSymbol.IsExtension)} instead.")] + // 4.14 BACKCOMPAT OVERLOAD -- DO NOT TOUCH + [EditorBrowsable(EditorBrowsableState.Never)] bool IsExtension { get; } - [Obsolete($"This API will be removed in the future. Use {nameof(INamedTypeSymbol)}.{nameof(INamedTypeSymbol.ExtensionParameter)} instead.")] + // 4.14 BACKCOMPAT OVERLOAD -- DO NOT TOUCH + [EditorBrowsable(EditorBrowsableState.Never)] IParameterSymbol? ExtensionParameter { get; } /// @@ -140,6 +143,8 @@ public interface ITypeSymbol : INamespaceOrTypeSymbol /// bool IsRecord { get; } + // https://github.com/dotnet/roslyn/issues/82636: Add bool IsUnion { get; } ? + /// /// Converts an ITypeSymbol and a nullable flow state to a string representation. /// diff --git a/src/Compilers/Core/Portable/Symbols/WellKnownMemberNames.cs b/src/Compilers/Core/Portable/Symbols/WellKnownMemberNames.cs index f68eaace25b3..d9d4b66a5788 100644 --- a/src/Compilers/Core/Portable/Symbols/WellKnownMemberNames.cs +++ b/src/Compilers/Core/Portable/Symbols/WellKnownMemberNames.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace Microsoft.CodeAnalysis @@ -418,6 +419,7 @@ public static class WellKnownMemberNames /// /// The required name for the property used in /// a ForEach statement when the collection is a nullable struct. + /// Also required name for the IUnion.Value property used in Union matching. /// public const string ValuePropertyName = "Value"; @@ -516,5 +518,17 @@ public static class WellKnownMemberNames /// The prefix for the marker type name. /// internal const string ExtensionMarkerTypePrefix = "$"; + + /// + /// The name for the 'HasValue' property. + /// + [Experimental("RSEXPERIMENTAL006", UrlFormat = "https://github.com/dotnet/roslyn/issues/82567")] + public const string HasValuePropertyName = "HasValue"; + + /// + /// The name for the 'TryGetValue' method. + /// + [Experimental("RSEXPERIMENTAL006", UrlFormat = "https://github.com/dotnet/roslyn/issues/82567")] + public const string TryGetValueMethodName = "TryGetValue"; } } diff --git a/src/Compilers/Core/Portable/Syntax/ChildSyntaxList.Enumerator.cs b/src/Compilers/Core/Portable/Syntax/ChildSyntaxList.Enumerator.cs index 7a2f965fe4a6..697c2e845c55 100644 --- a/src/Compilers/Core/Portable/Syntax/ChildSyntaxList.Enumerator.cs +++ b/src/Compilers/Core/Portable/Syntax/ChildSyntaxList.Enumerator.cs @@ -19,6 +19,7 @@ public struct Enumerator private int _count; private int _childIndex; private SlotData _slotData; + private Func? _descendIntoChildrenGreen; internal Enumerator(SyntaxNode node, int count) { @@ -26,16 +27,18 @@ internal Enumerator(SyntaxNode node, int count) _count = count; _childIndex = -1; _slotData = new SlotData(node); + _descendIntoChildrenGreen = null; } // PERF: Initialize an Enumerator directly from a SyntaxNode without going // via ChildNodesAndTokens. This saves constructing an intermediate ChildSyntaxList - internal void InitializeFrom(SyntaxNode node) + internal void InitializeFrom(SyntaxNode node, Func? descendIntoChildrenGreen) { _node = node; _count = CountNodes(node.Green); _childIndex = -1; _slotData = new SlotData(node); + _descendIntoChildrenGreen = descendIntoChildrenGreen; } /// Advances the enumerator to the next element of the . @@ -73,14 +76,23 @@ public void Reset() internal bool TryMoveNextAndGetCurrent(out SyntaxNodeOrToken current) { - if (!MoveNext()) + Debug.Assert(_node != null); + while (MoveNext()) { - current = default; - return false; + // When no green filter is present, every child passes unconditionally. When a + // green filter is active, check the child's green node first. A null green child + // (possible for absent items in list slots) is skipped because it cannot satisfy + // any filter (e.g. ContainsAnnotations). + if (_descendIntoChildrenGreen is null + || (GetGreenChildAt(_node, _childIndex, ref _slotData) is { } greenChild && _descendIntoChildrenGreen(greenChild))) + { + current = ItemInternal(_node, _childIndex, ref _slotData); + return true; + } } - current = ItemInternal(_node, _childIndex, ref _slotData); - return true; + current = default; + return false; } internal SyntaxNode? TryMoveNextAndGetCurrentAsNode() diff --git a/src/Compilers/Core/Portable/Syntax/ChildSyntaxList.cs b/src/Compilers/Core/Portable/Syntax/ChildSyntaxList.cs index 1e807b97deba..f3206c39909c 100644 --- a/src/Compilers/Core/Portable/Syntax/ChildSyntaxList.cs +++ b/src/Compilers/Core/Portable/Syntax/ChildSyntaxList.cs @@ -122,10 +122,16 @@ internal static SyntaxNodeOrToken ItemInternal(SyntaxNode node, int index) } /// - /// internal indexer that does not verify index. - /// Used when caller has already ensured that index is within bounds. + /// Walks green node slots to find the slot containing the child at the given index, updating slotData + /// for efficient subsequent lookups. Returns the green node for the slot and the index within that slot. /// - internal static SyntaxNodeOrToken ItemInternal(SyntaxNode node, int index, ref SlotData slotData) + private static GreenNode FindGreenSlotContainingIndex( + SyntaxNode node, + int index, + ref SlotData slotData, + out int slotIndex, + out int offsetInSlot, + out int position) { GreenNode? greenChild; var green = node.Green; @@ -133,11 +139,11 @@ internal static SyntaxNodeOrToken ItemInternal(SyntaxNode node, int index, ref S // slotData may contain information that allows us to start the loop below using data // calculated during a previous call. As index represents the offset into all children of // node, idx represents the offset requested relative to the given slot index. - var idx = index - slotData.PrecedingOccupantSlotCount; - var slotIndex = slotData.SlotIndex; - var position = slotData.PositionAtSlotIndex; + offsetInSlot = index - slotData.PrecedingOccupantSlotCount; + slotIndex = slotData.SlotIndex; + position = slotData.PositionAtSlotIndex; - Debug.Assert(idx >= 0); + Debug.Assert(offsetInSlot >= 0); // find a slot that contains the node or its parent list (if node is in a list) // we will be skipping whole slots here so we will not loop for long @@ -154,12 +160,12 @@ internal static SyntaxNodeOrToken ItemInternal(SyntaxNode node, int index, ref S if (greenChild != null) { int currentOccupancy = Occupancy(greenChild); - if (idx < currentOccupancy) + if (offsetInSlot < currentOccupancy) { break; } - idx -= currentOccupancy; + offsetInSlot -= currentOccupancy; position += greenChild.FullWidth; } @@ -169,9 +175,33 @@ internal static SyntaxNodeOrToken ItemInternal(SyntaxNode node, int index, ref S if (slotIndex != slotData.SlotIndex) { // (index - idx) represents the number of occupants prior to this new slotIndex - slotData = new SlotData(slotIndex, index - idx, position); + slotData = new SlotData(slotIndex, index - offsetInSlot, position); } + return greenChild; + } + + /// + /// Returns the green node for the child at the given index without creating any red nodes. + /// For list slots, returns the individual item's green node within the list. + /// Updates slotData for efficient subsequent lookups. + /// + private static GreenNode GetGreenChildAt(SyntaxNode node, int index, ref SlotData slotData) + { + var greenSlot = FindGreenSlotContainingIndex(node, index, ref slotData, out _, out var offsetInSlot, out _); + var result = greenSlot.IsList ? greenSlot.GetSlot(offsetInSlot) : greenSlot; + Debug.Assert(result != null); + return result; + } + + /// + /// internal indexer that does not verify index. + /// Used when caller has already ensured that index is within bounds. + /// + internal static SyntaxNodeOrToken ItemInternal(SyntaxNode node, int index, ref SlotData slotData) + { + var greenChild = FindGreenSlotContainingIndex(node, index, ref slotData, out var slotIndex, out var idx, out var position); + // get node that represents this slot var red = node.GetNodeSlot(slotIndex); if (!greenChild.IsList) @@ -302,52 +332,14 @@ internal static SyntaxNodeOrToken ChildThatContainsPosition(SyntaxNode node, int /// internal static SyntaxNode? ItemInternalAsNode(SyntaxNode node, int index, ref SlotData slotData) { - GreenNode? greenChild; - var green = node.Green; - var idx = index - slotData.PrecedingOccupantSlotCount; - var slotIndex = slotData.SlotIndex; - var position = slotData.PositionAtSlotIndex; - - Debug.Assert(idx >= 0); - - // find a slot that contains the node or its parent list (if node is in a list) - // we will be skipping whole slots here so we will not loop for long - // the max possible number of slots is 11 (TypeDeclarationSyntax) - // and typically much less than that - // - // at the end of this loop we will have - // 1) slot index - slotIdx - // 2) if the slot is a list, node index in the list - idx - while (true) - { - greenChild = green.GetSlot(slotIndex); - if (greenChild != null) - { - int currentOccupancy = Occupancy(greenChild); - if (idx < currentOccupancy) - { - break; - } - - idx -= currentOccupancy; - position += greenChild.FullWidth; - } - - slotIndex++; - } - - if (slotIndex != slotData.SlotIndex) - { - // (index - idx) represents the number of occupants prior to this new slotIndex - slotData = new SlotData(slotIndex, index - idx, position); - } + var greenChild = FindGreenSlotContainingIndex(node, index, ref slotData, out var slotIndex, out var offsetInSlot, out _); // get node that represents this slot var red = node.GetNodeSlot(slotIndex); if (greenChild.IsList && red != null) { // it is a red list of nodes (separated or not), most common case - return red.GetNodeSlot(idx); + return red.GetNodeSlot(offsetInSlot); } // this is a single node or token diff --git a/src/Compilers/Core/Portable/Syntax/GreenNode.cs b/src/Compilers/Core/Portable/Syntax/GreenNode.cs index b5a1b4fe5baf..bdcb6cf62550 100644 --- a/src/Compilers/Core/Portable/Syntax/GreenNode.cs +++ b/src/Compilers/Core/Portable/Syntax/GreenNode.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Text; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Syntax.InternalSyntax; @@ -50,6 +51,10 @@ private string GetDebuggerDisplay() private static readonly SyntaxAnnotation[] s_noAnnotations = Array.Empty(); private static readonly IEnumerable s_noAnnotationsEnumerable = SpecializedCollections.EmptyEnumerable(); + // Pool of StringWriters to reduce allocations during ToString/ToFullString + private static readonly ObjectPool s_stringWriterPool = + new ObjectPool(() => new StringWriter(new StringBuilder(), System.Globalization.CultureInfo.InvariantCulture)); + protected GreenNode(ushort kind) { _kind = kind; @@ -610,18 +615,30 @@ internal DiagnosticInfo[] GetDiagnostics() public virtual string ToFullString() { - var sb = PooledStringBuilder.GetInstance(); - var writer = new System.IO.StringWriter(sb.Builder, System.Globalization.CultureInfo.InvariantCulture); + var writer = s_stringWriterPool.Allocate(); + var sb = writer.GetStringBuilder(); + this.WriteTo(writer, leading: true, trailing: true); - return sb.ToStringAndFree(); + var result = sb.ToString(); + + sb.Clear(); + s_stringWriterPool.Free(writer); + + return result; } public override string ToString() { - var sb = PooledStringBuilder.GetInstance(); - var writer = new System.IO.StringWriter(sb.Builder, System.Globalization.CultureInfo.InvariantCulture); + var writer = s_stringWriterPool.Allocate(); + var sb = writer.GetStringBuilder(); + this.WriteTo(writer, leading: false, trailing: false); - return sb.ToStringAndFree(); + var result = sb.ToString(); + + sb.Clear(); + s_stringWriterPool.Free(writer); + + return result; } public void WriteTo(System.IO.TextWriter writer) @@ -962,71 +979,6 @@ public SyntaxNode CreateRed() #endregion - #region Caching - - internal const int MaxCachedChildNum = 3; - - internal bool IsCacheable - { - get - { - return ((this.Flags & NodeFlags.InheritMask) == NodeFlags.IsNotMissing) && - this.SlotCount <= GreenNode.MaxCachedChildNum; - } - } - - internal int GetCacheHash() - { - Debug.Assert(this.IsCacheable); - - int code = (int)(this.Flags) ^ this.RawKind; - int cnt = this.SlotCount; - for (int i = 0; i < cnt; i++) - { - var child = GetSlot(i); - if (child != null) - { - code = Hash.Combine(RuntimeHelpers.GetHashCode(child), code); - } - } - - return code & Int32.MaxValue; - } - - internal bool IsCacheEquivalent(int kind, NodeFlags flags, GreenNode? child1) - { - Debug.Assert(this.IsCacheable); - - return this.RawKind == kind && - this.Flags == flags && - this.SlotCount == 1 && - this.GetSlot(0) == child1; - } - - internal bool IsCacheEquivalent(int kind, NodeFlags flags, GreenNode? child1, GreenNode? child2) - { - Debug.Assert(this.IsCacheable); - - return this.RawKind == kind && - this.Flags == flags && - this.SlotCount == 2 && - this.GetSlot(0) == child1 && - this.GetSlot(1) == child2; - } - - internal bool IsCacheEquivalent(int kind, NodeFlags flags, GreenNode? child1, GreenNode? child2, GreenNode? child3) - { - Debug.Assert(this.IsCacheable); - - return this.RawKind == kind && - this.Flags == flags && - this.SlotCount == 3 && - this.GetSlot(0) == child1 && - this.GetSlot(1) == child2 && - this.GetSlot(2) == child3; - } - #endregion //Caching - /// /// Add an error to the given node, creating a new node that is the same except it has no parent, /// and has the given error attached to it. The error span is the entire span of this node. diff --git a/src/Compilers/Core/Portable/Syntax/InternalSyntax/SyntaxNodeCache.cs b/src/Compilers/Core/Portable/Syntax/InternalSyntax/SyntaxNodeCache.cs index 31d64af4828c..8ff91870331d 100644 --- a/src/Compilers/Core/Portable/Syntax/InternalSyntax/SyntaxNodeCache.cs +++ b/src/Compilers/Core/Portable/Syntax/InternalSyntax/SyntaxNodeCache.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.CodeAnalysis.Syntax.InternalSyntax; - using System; using System.Diagnostics; +using System.Runtime.CompilerServices; using Roslyn.Utilities; #if STATS @@ -38,8 +37,21 @@ namespace Microsoft.CodeAnalysis.Syntax.InternalSyntax /// We only consider "normal" nodes to be cacheable. /// Nodes with diagnostics/annotations/directives/skipped, etc... have more complicated identity /// and are not likely to be repetitive. - /// /// + /// + /// The use of , (and any provided child nodes) + /// ensures during lookup that we only return a node that is identical in all relevant aspects to the node that + /// we're about to create otherwise. This is a brittle guarantee. However, given how locked down green nodes are, + /// this seems acceptable for now. Great care needs to be taken if new properties are added to green nodes that + /// would affect their identity. + /// + /// Only nodes created through SyntaxFactory methods are cached. Nodes created directly through their constructors + /// are not cached. This is fairly intuitive as a constructor would not be able to somehow return some other + /// instance different than the one being constructed. A subtle aspect of this though is that this is what ensures + /// that nodes with diagnostics or annotations on them are not cached. These nodes are created starting with + /// another node and forking it to add diagnostics/annotations. This forking always calls through a constructor and + /// not a factory method. And as such, never comes through here. + /// internal class GreenStats { // TODO: remove when done tweaking this cache. @@ -114,19 +126,14 @@ internal static class SyntaxNodeCache private const int CacheSize = 1 << CacheSizeBits; private const int CacheMask = CacheSize - 1; - private readonly struct Entry - { - public readonly int hash; - public readonly GreenNode? node; - - internal Entry(int hash, GreenNode node) - { - this.hash = hash; - this.node = node; - } - } - - private static readonly Entry[] s_cache = new Entry[CacheSize]; + /// + /// Simple array indexed by the hash of the cached node. Note that unlike a typical dictionary/hashtable, this + /// does not exercise any form of collision resolution. If two different nodes hash to the same index, the + /// latter will overwrite the former. This is acceptable since this is just an opportunistic cache. Reads from + /// the cache validate that the node they get back is actually the one they were looking for. See the comments + /// in for more details. + /// + private static readonly GreenNode[] s_cache = new GreenNode[CacheSize]; internal static void AddNode(GreenNode node, int hash) { @@ -134,16 +141,16 @@ internal static void AddNode(GreenNode node, int hash) { GreenStats.ItemAdded(); - Debug.Assert(node.GetCacheHash() == hash); + Debug.Assert(GetCacheHash(node) == hash); var idx = hash & CacheMask; - s_cache[idx] = new Entry(hash, node); + s_cache[idx] = node; } } private static bool CanBeCached(GreenNode? child1) { - return child1 == null || child1.IsCacheable; + return child1 == null || IsCacheable(child1); } private static bool CanBeCached(GreenNode? child1, GreenNode? child2) @@ -163,9 +170,9 @@ private static bool ChildInCache(GreenNode? child) // TODO: should use slotCount if (child == null || child.SlotCount == 0) return true; - int hash = child.GetCacheHash(); + int hash = GetCacheHash(child); int idx = hash & CacheMask; - return s_cache[idx].node == child; + return s_cache[idx] == child; } private static bool AllChildrenInCache(GreenNode node) @@ -194,13 +201,18 @@ private static bool AllChildrenInCache(GreenNode node) { GreenStats.ItemCacheable(); + // Determine the hash for the node being created, given its kind, flags, and optional single child. Then + // grab out a potential cached node from the cache based on that hash. Note that this may not actually + // be a viable match due to potential hash collisions, where we have 'last one wins' semantics. So if + // we do see a node in the cache, we have to validate that it is actually equivalent to the data + // being used to populate the cache entry. This is what IsCacheEquivalent is for. It allows us to check + // that the node already there has that same kind, flags, and the same child (by reference). int h = hash = GetCacheHash(kind, flags, child1); - int idx = h & CacheMask; - var e = s_cache[idx]; - if (e.hash == h && e.node != null && e.node.IsCacheEquivalent(kind, flags, child1)) + var e = s_cache[h & CacheMask]; + if (IsCacheEquivalent(e, kind, flags, child1)) { GreenStats.CacheHit(); - return e.node; + return e; } } else @@ -223,12 +235,11 @@ private static bool AllChildrenInCache(GreenNode node) GreenStats.ItemCacheable(); int h = hash = GetCacheHash(kind, flags, child1, child2); - int idx = h & CacheMask; - var e = s_cache[idx]; - if (e.hash == h && e.node != null && e.node.IsCacheEquivalent(kind, flags, child1, child2)) + var e = s_cache[h & CacheMask]; + if (IsCacheEquivalent(e, kind, flags, child1, child2)) { GreenStats.CacheHit(); - return e.node; + return e; } } else @@ -251,12 +262,11 @@ private static bool AllChildrenInCache(GreenNode node) GreenStats.ItemCacheable(); int h = hash = GetCacheHash(kind, flags, child1, child2, child3); - int idx = h & CacheMask; - var e = s_cache[idx]; - if (e.hash == h && e.node != null && e.node.IsCacheEquivalent(kind, flags, child1, child2, child3)) + var e = s_cache[h & CacheMask]; + if (IsCacheEquivalent(e, kind, flags, child1, child2, child3)) { GreenStats.CacheHit(); - return e.node; + return e; } } else @@ -318,5 +328,76 @@ private static int GetCacheHash(int kind, GreenNode.NodeFlags flags, GreenNode? // ensure nonnegative hash return code & Int32.MaxValue; } + + private const int MaxCachedChildNum = 3; + + private static bool IsCacheable(GreenNode node) + { + return ((node.Flags & GreenNode.NodeFlags.InheritMask) == GreenNode.NodeFlags.IsNotMissing) && + node.SlotCount <= MaxCachedChildNum; + } + + /// + /// Internal for testing purposes only. Do not use outside of this type or tests. + /// + internal static int GetCacheHash(GreenNode node) + { + Debug.Assert(IsCacheable(node)); + + int code = (int)(node.Flags) ^ node.RawKind; + int cnt = node.SlotCount; + for (int i = 0; i < cnt; i++) + { + var child = node.GetSlot(i); + if (child != null) + { + code = Hash.Combine(RuntimeHelpers.GetHashCode(child), code); + } + } + + return code & Int32.MaxValue; + } + + private static bool IsCacheEquivalent(GreenNode? parent, int kind, GreenNode.NodeFlags flags, GreenNode? child1) + { + if (parent is null) + return false; + + Debug.Assert(IsCacheable(parent)); + + return parent.RawKind == kind && + parent.Flags == flags && + parent.SlotCount == 1 && + parent.GetSlot(0) == child1; + } + + private static bool IsCacheEquivalent(GreenNode? parent, int kind, GreenNode.NodeFlags flags, GreenNode? child1, GreenNode? child2) + { + if (parent is null) + return false; + + Debug.Assert(IsCacheable(parent)); + + return parent.RawKind == kind && + parent.Flags == flags && + parent.SlotCount == 2 && + parent.GetSlot(0) == child1 && + parent.GetSlot(1) == child2; + } + + private static bool IsCacheEquivalent(GreenNode? parent, int kind, GreenNode.NodeFlags flags, GreenNode? child1, GreenNode? child2, GreenNode? child3) + { + if (parent is null) + return false; + + Debug.Assert(IsCacheable(parent)); + + return parent.RawKind == kind && + parent.Flags == flags && + parent.SlotCount == 3 && + parent.GetSlot(0) == child1 && + parent.GetSlot(1) == child2 && + parent.GetSlot(2) == child3; + } } } diff --git a/src/Compilers/Core/Portable/Syntax/SyntaxNode.Iterators.cs b/src/Compilers/Core/Portable/Syntax/SyntaxNode.Iterators.cs index 90d59f5b4fc5..f68c6ab410cf 100644 --- a/src/Compilers/Core/Portable/Syntax/SyntaxNode.Iterators.cs +++ b/src/Compilers/Core/Portable/Syntax/SyntaxNode.Iterators.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -8,7 +8,6 @@ using System.Linq; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis { @@ -17,22 +16,36 @@ public abstract partial class SyntaxNode private IEnumerable DescendantNodesImpl(TextSpan span, Func? descendIntoChildren, bool descendIntoTrivia, bool includeSelf) { return descendIntoTrivia - ? DescendantNodesAndTokensImpl(span, descendIntoChildren, true, includeSelf).Where(e => e.IsNode).Select(e => e.AsNode()!) + ? DescendantNodesAndTokensImpl(span, descendIntoChildren, descendIntoTrivia: true, includeSelf).Where(static e => e.IsNode).Select(static e => e.AsNode()!) : DescendantNodesOnly(span, descendIntoChildren, includeSelf); } private IEnumerable DescendantNodesAndTokensImpl(TextSpan span, Func? descendIntoChildren, bool descendIntoTrivia, bool includeSelf) + { + return DescendantNodesAndTokensImpl(span, descendIntoChildrenGreen: null, descendIntoChildren, descendIntoTrivia, includeSelf); + } + + private IEnumerable DescendantNodesAndTokensImpl( + TextSpan span, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed, + bool descendIntoTrivia, + bool includeSelf) { return descendIntoTrivia - ? DescendantNodesAndTokensIntoTrivia(span, descendIntoChildren, includeSelf) - : DescendantNodesAndTokensOnly(span, descendIntoChildren, includeSelf); + ? DescendantNodesAndTokensIntoTrivia(span, descendIntoChildrenGreen, descendIntoChildrenRed, includeSelf) + : DescendantNodesAndTokensOnly(span, descendIntoChildrenGreen, descendIntoChildrenRed, includeSelf); } - private IEnumerable DescendantTriviaImpl(TextSpan span, Func? descendIntoChildren = null, bool descendIntoTrivia = false) + private IEnumerable DescendantTriviaImpl( + TextSpan span, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed, + bool descendIntoTrivia = false) { return descendIntoTrivia - ? DescendantTriviaIntoTrivia(span, descendIntoChildren) - : DescendantTriviaOnly(span, descendIntoChildren); + ? DescendantTriviaIntoTrivia(span, descendIntoChildrenGreen, descendIntoChildrenRed) + : DescendantTriviaOnly(span, descendIntoChildrenGreen, descendIntoChildrenRed); } private static bool IsInSpan(in TextSpan span, TextSpan childSpan) @@ -46,16 +59,36 @@ private struct ChildSyntaxListEnumeratorStack : IDisposable { private static readonly ObjectPool s_stackPool = new ObjectPool(() => new ChildSyntaxList.Enumerator[16]); + /// + /// Optional green-node predicate checked before creating a red node for a child. + /// Allows skipping entire subtrees without allocating red nodes. + /// is equivalent to a predicate that always returns . + /// + private readonly Func? _descendIntoChildrenGreen; + + /// + /// Optional red-node predicate checked before descending into a node's children. + /// Only invoked when the green filter check passes. is + /// equivalent to a predicate that always returns . + /// + private readonly Func? _descendIntoChildrenRed; + private ChildSyntaxList.Enumerator[]? _stack; private int _stackPtr; - public ChildSyntaxListEnumeratorStack(SyntaxNode startingNode, Func? descendIntoChildren) + public ChildSyntaxListEnumeratorStack( + SyntaxNode startingNode, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed) { - if (descendIntoChildren == null || descendIntoChildren(startingNode)) + _descendIntoChildrenGreen = descendIntoChildrenGreen; + _descendIntoChildrenRed = descendIntoChildrenRed; + + if (ShouldDescendIntoChildren(startingNode)) { _stack = s_stackPool.Allocate(); _stackPtr = 0; - _stack[0].InitializeFrom(startingNode); + _stack[0].InitializeFrom(startingNode, descendIntoChildrenGreen); } else { @@ -97,8 +130,15 @@ public bool TryGetNextInSpan(in TextSpan span, out SyntaxNodeOrToken value) return null; } - public void PushChildren(SyntaxNode node) + private bool ShouldDescendIntoChildren(SyntaxNode node) + => _descendIntoChildrenGreen?.Invoke(node.Green) is not false + && _descendIntoChildrenRed?.Invoke(node) is not false; + + public bool PushChildren(SyntaxNode node) { + if (!ShouldDescendIntoChildren(node)) + return false; + Debug.Assert(_stack is object); if (++_stackPtr >= _stack.Length) { @@ -106,15 +146,8 @@ public void PushChildren(SyntaxNode node) Array.Resize(ref _stack, checked(_stackPtr * 2)); } - _stack[_stackPtr].InitializeFrom(node); - } - - public void PushChildren(SyntaxNode node, Func? descendIntoChildren) - { - if (descendIntoChildren == null || descendIntoChildren(node)) - { - PushChildren(node); - } + _stack[_stackPtr].InitializeFrom(node, _descendIntoChildrenGreen); + return true; } public void Dispose() @@ -196,9 +229,12 @@ public enum Which : byte private TriviaListEnumeratorStack _triviaStack; private readonly ArrayBuilder? _discriminatorStack; - public TwoEnumeratorListStack(SyntaxNode startingNode, Func? descendIntoChildren) + public TwoEnumeratorListStack( + SyntaxNode startingNode, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed) { - _nodeStack = new ChildSyntaxListEnumeratorStack(startingNode, descendIntoChildren); + _nodeStack = new ChildSyntaxListEnumeratorStack(startingNode, descendIntoChildrenGreen, descendIntoChildrenRed); _triviaStack = new TriviaListEnumeratorStack(); if (_nodeStack.IsNotEmpty) { @@ -243,12 +279,11 @@ public bool TryGetNext(out SyntaxTrivia value) return false; } - public void PushChildren(SyntaxNode node, Func? descendIntoChildren) + public void PushChildren(SyntaxNode node) { - if (descendIntoChildren == null || descendIntoChildren(node)) + if (_nodeStack.PushChildren(node)) { Debug.Assert(_discriminatorStack is object); - _nodeStack.PushChildren(node); _discriminatorStack.Push(Which.Node); } } @@ -289,9 +324,12 @@ public enum Which : byte private readonly ArrayBuilder? _tokenStack; private readonly ArrayBuilder? _discriminatorStack; - public ThreeEnumeratorListStack(SyntaxNode startingNode, Func? descendIntoChildren) + public ThreeEnumeratorListStack( + SyntaxNode startingNode, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed) { - _nodeStack = new ChildSyntaxListEnumeratorStack(startingNode, descendIntoChildren); + _nodeStack = new ChildSyntaxListEnumeratorStack(startingNode, descendIntoChildrenGreen, descendIntoChildrenRed); _triviaStack = new TriviaListEnumeratorStack(); if (_nodeStack.IsNotEmpty) { @@ -346,12 +384,11 @@ public SyntaxNodeOrToken PopToken() return _tokenStack.Pop(); } - public void PushChildren(SyntaxNode node, Func? descendIntoChildren) + public void PushChildren(SyntaxNode node) { - if (descendIntoChildren == null || descendIntoChildren(node)) + if (_nodeStack.PushChildren(node)) { Debug.Assert(_discriminatorStack is object); - _nodeStack.PushChildren(node); _discriminatorStack.Push(Which.Node); } } @@ -394,7 +431,7 @@ private IEnumerable DescendantNodesOnly(TextSpan span, Func DescendantNodesOnly(TextSpan span, Func DescendantNodesOnly(TextSpan span, Func DescendantNodesAndTokensOnly(TextSpan span, Func? descendIntoChildren, bool includeSelf) + private bool ShouldYieldSelf(bool includeSelf, in TextSpan span, Func? descendIntoChildrenGreen) + => includeSelf && descendIntoChildrenGreen?.Invoke(this.Green) is not false && IsInSpan(in span, this.FullSpan); + + private IEnumerable DescendantNodesAndTokensOnly( + TextSpan span, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed, + bool includeSelf) { - if (includeSelf && IsInSpan(in span, this.FullSpan)) + if (ShouldYieldSelf(includeSelf, in span, descendIntoChildrenGreen)) { yield return this; } - using (var stack = new ChildSyntaxListEnumeratorStack(this, descendIntoChildren)) + using (var stack = new ChildSyntaxListEnumeratorStack(this, descendIntoChildrenGreen, descendIntoChildrenRed)) { while (stack.IsNotEmpty) { @@ -432,7 +476,7 @@ private IEnumerable DescendantNodesAndTokensOnly(TextSpan spa var nodeValue = value.AsNode(); if (nodeValue != null) { - stack.PushChildren(nodeValue, descendIntoChildren); + stack.PushChildren(nodeValue); } yield return value; @@ -441,14 +485,18 @@ private IEnumerable DescendantNodesAndTokensOnly(TextSpan spa } } - private IEnumerable DescendantNodesAndTokensIntoTrivia(TextSpan span, Func? descendIntoChildren, bool includeSelf) + private IEnumerable DescendantNodesAndTokensIntoTrivia( + TextSpan span, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed, + bool includeSelf) { - if (includeSelf && IsInSpan(in span, this.FullSpan)) + if (ShouldYieldSelf(includeSelf, in span, descendIntoChildrenGreen)) { yield return this; } - using (var stack = new ThreeEnumeratorListStack(this, descendIntoChildren)) + using (var stack = new ThreeEnumeratorListStack(this, descendIntoChildrenGreen, descendIntoChildrenRed)) { while (stack.IsNotEmpty) { @@ -464,7 +512,7 @@ private IEnumerable DescendantNodesAndTokensIntoTrivia(TextSp if (value.IsNode) { // parent nodes come before children (prefix document order) - stack.PushChildren(value.AsNode()!, descendIntoChildren); + stack.PushChildren(value.AsNode()!); } else if (value.IsToken) { @@ -514,7 +562,7 @@ private IEnumerable DescendantNodesAndTokensIntoTrivia(TextSp // PERF: Push before yield return so that "structureNode" is 'dead' after the yield // and therefore doesn't need to be stored in the iterator state machine. This // saves a field. - stack.PushChildren(structureNode, descendIntoChildren); + stack.PushChildren(structureNode); yield return structureNode; } @@ -529,9 +577,12 @@ private IEnumerable DescendantNodesAndTokensIntoTrivia(TextSp } } - private IEnumerable DescendantTriviaOnly(TextSpan span, Func? descendIntoChildren) + private IEnumerable DescendantTriviaOnly( + TextSpan span, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed) { - using (var stack = new ChildSyntaxListEnumeratorStack(this, descendIntoChildren)) + using (var stack = new ChildSyntaxListEnumeratorStack(this, descendIntoChildrenGreen, descendIntoChildrenRed)) { while (stack.IsNotEmpty) { @@ -540,7 +591,7 @@ private IEnumerable DescendantTriviaOnly(TextSpan span, Func DescendantTriviaOnly(TextSpan span, Func DescendantTriviaIntoTrivia(TextSpan span, Func? descendIntoChildren) + private IEnumerable DescendantTriviaIntoTrivia( + TextSpan span, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed) { - using (var stack = new TwoEnumeratorListStack(this, descendIntoChildren)) + using (var stack = new TwoEnumeratorListStack(this, descendIntoChildrenGreen, descendIntoChildrenRed)) { while (stack.IsNotEmpty) { @@ -581,7 +635,7 @@ private IEnumerable DescendantTriviaIntoTrivia(TextSpan span, Func { if (value.AsNode(out var nodeValue)) { - stack.PushChildren(nodeValue, descendIntoChildren); + stack.PushChildren(nodeValue); } else if (value.IsToken) { @@ -611,7 +665,7 @@ private IEnumerable DescendantTriviaIntoTrivia(TextSpan span, Func // saves a field. if (trivia.TryGetStructure(out var structureNode)) { - stack.PushChildren(structureNode, descendIntoChildren); + stack.PushChildren(structureNode); } if (IsInSpan(in span, trivia.FullSpan)) diff --git a/src/Compilers/Core/Portable/Syntax/SyntaxNode.cs b/src/Compilers/Core/Portable/Syntax/SyntaxNode.cs index 9dfd15651875..c08fa1fca27a 100644 --- a/src/Compilers/Core/Portable/Syntax/SyntaxNode.cs +++ b/src/Compilers/Core/Portable/Syntax/SyntaxNode.cs @@ -1184,7 +1184,7 @@ internal static SyntaxTrivia FindTriviaByOffset(SyntaxNode node, int textOffset, /// public IEnumerable DescendantTrivia(Func? descendIntoChildren = null, bool descendIntoTrivia = false) { - return DescendantTriviaImpl(this.FullSpan, descendIntoChildren, descendIntoTrivia); + return DescendantTriviaImpl(this.FullSpan, descendIntoChildrenGreen: null, descendIntoChildren, descendIntoTrivia); } /// @@ -1192,7 +1192,30 @@ public IEnumerable DescendantTrivia(Func? descen /// public IEnumerable DescendantTrivia(TextSpan span, Func? descendIntoChildren = null, bool descendIntoTrivia = false) { - return DescendantTriviaImpl(span, descendIntoChildren, descendIntoTrivia); + return DescendantTriviaImpl(span, descendIntoChildrenGreen: null, descendIntoChildren, descendIntoTrivia); + } + + /// + /// Get a list of all the trivia associated with the descendant nodes and tokens. + /// + internal IEnumerable DescendantTrivia( + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed, + bool descendIntoTrivia = false) + { + return DescendantTriviaImpl(this.FullSpan, descendIntoChildrenGreen, descendIntoChildrenRed, descendIntoTrivia); + } + + /// + /// Get a list of all the trivia associated with the descendant nodes and tokens. + /// + internal IEnumerable DescendantTrivia( + TextSpan span, + Func? descendIntoChildrenGreen, + Func? descendIntoChildrenRed, + bool descendIntoTrivia = false) + { + return DescendantTriviaImpl(span, descendIntoChildrenGreen, descendIntoChildrenRed, descendIntoTrivia); } #endregion @@ -1252,12 +1275,22 @@ internal SyntaxAnnotation[] GetAnnotations() return this.Green.GetAnnotations(); } + /// + /// Gets a list of descendant nodes and tokens (including this node) in prefix document order, + /// filtered at the green node level. Children whose green nodes do not pass + /// are skipped entirely (not yielded and not descended into), avoiding red node creation for those subtrees. + /// + internal IEnumerable DescendantNodesAndTokensAndSelf(Func descendIntoChildrenGreen, bool descendIntoTrivia) + { + return DescendantNodesAndTokensImpl(this.FullSpan, descendIntoChildrenGreen, descendIntoChildrenRed: null, descendIntoTrivia, includeSelf: true); + } + /// /// Gets all nodes and tokens with an annotation of the specified annotation kind. /// public IEnumerable GetAnnotatedNodesAndTokens(string annotationKind) { - return this.DescendantNodesAndTokensAndSelf(n => n.ContainsAnnotations, descendIntoTrivia: true) + return this.DescendantNodesAndTokensAndSelf(descendIntoChildrenGreen: static g => g.ContainsAnnotations, descendIntoTrivia: true) .Where(t => t.HasAnnotations(annotationKind)); } @@ -1266,7 +1299,7 @@ public IEnumerable GetAnnotatedNodesAndTokens(string annotati /// public IEnumerable GetAnnotatedNodesAndTokens(params string[] annotationKinds) { - return this.DescendantNodesAndTokensAndSelf(n => n.ContainsAnnotations, descendIntoTrivia: true) + return this.DescendantNodesAndTokensAndSelf(descendIntoChildrenGreen: static g => g.ContainsAnnotations, descendIntoTrivia: true) .Where(t => t.HasAnnotations(annotationKinds)); } @@ -1275,7 +1308,7 @@ public IEnumerable GetAnnotatedNodesAndTokens(params string[] /// public IEnumerable GetAnnotatedNodesAndTokens(SyntaxAnnotation annotation) { - return this.DescendantNodesAndTokensAndSelf(n => n.ContainsAnnotations, descendIntoTrivia: true) + return this.DescendantNodesAndTokensAndSelf(descendIntoChildrenGreen: static g => g.ContainsAnnotations, descendIntoTrivia: true) .Where(t => t.HasAnnotation(annotation)); } @@ -1318,7 +1351,7 @@ public IEnumerable GetAnnotatedTokens(string annotationKind) /// public IEnumerable GetAnnotatedTrivia(string annotationKind) { - return this.DescendantTrivia(n => n.ContainsAnnotations, descendIntoTrivia: true) + return this.DescendantTrivia(descendIntoChildrenGreen: static n => n.ContainsAnnotations, descendIntoChildrenRed: null, descendIntoTrivia: true) .Where(tr => tr.HasAnnotations(annotationKind)); } @@ -1327,7 +1360,7 @@ public IEnumerable GetAnnotatedTrivia(string annotationKind) /// public IEnumerable GetAnnotatedTrivia(params string[] annotationKinds) { - return this.DescendantTrivia(n => n.ContainsAnnotations, descendIntoTrivia: true) + return this.DescendantTrivia(descendIntoChildrenGreen: static n => n.ContainsAnnotations, descendIntoChildrenRed: null, descendIntoTrivia: true) .Where(tr => tr.HasAnnotations(annotationKinds)); } @@ -1336,7 +1369,7 @@ public IEnumerable GetAnnotatedTrivia(params string[] annotationKi /// public IEnumerable GetAnnotatedTrivia(SyntaxAnnotation annotation) { - return this.DescendantTrivia(n => n.ContainsAnnotations, descendIntoTrivia: true) + return this.DescendantTrivia(descendIntoChildrenGreen: static n => n.ContainsAnnotations, descendIntoChildrenRed: null, descendIntoTrivia: true) .Where(tr => tr.HasAnnotation(annotation)); } diff --git a/src/Compilers/Core/Portable/Syntax/SyntaxNodeOrToken.cs b/src/Compilers/Core/Portable/Syntax/SyntaxNodeOrToken.cs index e8b6927b923c..a64f76301d13 100644 --- a/src/Compilers/Core/Portable/Syntax/SyntaxNodeOrToken.cs +++ b/src/Compilers/Core/Portable/Syntax/SyntaxNodeOrToken.cs @@ -869,7 +869,7 @@ private static void GetDirectives(in SyntaxNodeOrToken node, Func(SyntaxNode node, Func? filter, ref List? directives) where TDirective : SyntaxNode { - foreach (var trivia in node.DescendantTrivia(node => node.ContainsDirectives, descendIntoTrivia: true)) + foreach (var trivia in node.DescendantTrivia(descendIntoChildrenGreen: static node => node.ContainsDirectives, descendIntoChildrenRed: null, descendIntoTrivia: true)) { GetDirectivesInTrivia(trivia, filter, ref directives); } diff --git a/src/Compilers/Core/Portable/Text/SourceHashAlgorithm.cs b/src/Compilers/Core/Portable/Text/SourceHashAlgorithm.cs index cdcc18b9ec3f..7d95aae08eb8 100644 --- a/src/Compilers/Core/Portable/Text/SourceHashAlgorithm.cs +++ b/src/Compilers/Core/Portable/Text/SourceHashAlgorithm.cs @@ -20,8 +20,18 @@ public enum SourceHashAlgorithm Sha1 = 1, /// - /// Secure Hash Algorithm 2 with a hash size of 256 bits. + /// Secure Hash Algorithm with a hash size of 256 bits. /// Sha256 = 2, + + /// + /// Secure Hash Algorithm with a hash size of 384 bits. + /// + Sha384 = 3, + + /// + /// Secure Hash Algorithm with a hash size of 512 bits. + /// + Sha512 = 4, } } diff --git a/src/Compilers/Core/Portable/Text/SourceHashAlgorithms.cs b/src/Compilers/Core/Portable/Text/SourceHashAlgorithms.cs index 993ed5bcdd93..f1f23cd350b5 100644 --- a/src/Compilers/Core/Portable/Text/SourceHashAlgorithms.cs +++ b/src/Compilers/Core/Portable/Text/SourceHashAlgorithms.cs @@ -24,12 +24,16 @@ internal static class SourceHashAlgorithms private static readonly Guid s_guidSha1 = unchecked(new Guid((int)0xff1816ec, (short)0xaa5e, 0x4d10, 0x87, 0xf7, 0x6f, 0x49, 0x63, 0x83, 0x34, 0x60)); private static readonly Guid s_guidSha256 = unchecked(new Guid((int)0x8829d00f, 0x11b8, 0x4213, 0x87, 0x8b, 0x77, 0x0e, 0x85, 0x97, 0xac, 0x16)); + private static readonly Guid s_guidSha384 = unchecked(new Guid((int)0xd99cfeb1, (short)0x8c43, 0x444a, 0x8a, 0x6c, 0xb6, 0x12, 0x69, 0xd2, 0xa0, 0xbf)); + private static readonly Guid s_guidSha512 = unchecked(new Guid((int)0xef2d1afc, 0x6550, 0x46d6, 0xb1, 0x4b, 0xd7, 0x0a, 0xfe, 0x9a, 0x55, 0x66)); public static bool IsSupportedAlgorithm(SourceHashAlgorithm algorithm) => algorithm switch { SourceHashAlgorithm.Sha1 => true, SourceHashAlgorithm.Sha256 => true, + SourceHashAlgorithm.Sha384 => true, + SourceHashAlgorithm.Sha512 => true, _ => false }; @@ -38,12 +42,16 @@ public static Guid GetAlgorithmGuid(SourceHashAlgorithm algorithm) { SourceHashAlgorithm.Sha1 => s_guidSha1, SourceHashAlgorithm.Sha256 => s_guidSha256, + SourceHashAlgorithm.Sha384 => s_guidSha384, + SourceHashAlgorithm.Sha512 => s_guidSha512, _ => throw ExceptionUtilities.UnexpectedValue(algorithm), }; public static SourceHashAlgorithm GetSourceHashAlgorithm(Guid guid) => (guid == s_guidSha256) ? SourceHashAlgorithm.Sha256 : (guid == s_guidSha1) ? SourceHashAlgorithm.Sha1 : + (guid == s_guidSha384) ? SourceHashAlgorithm.Sha384 : + (guid == s_guidSha512) ? SourceHashAlgorithm.Sha512 : SourceHashAlgorithm.None; private static HashAlgorithm CreateInstance(SourceHashAlgorithm algorithm) @@ -53,6 +61,8 @@ private static HashAlgorithm CreateInstance(SourceHashAlgorithm algorithm) // CodeQL [SM02196] This is not enabled by default but exists as a compat option for existing builds. SourceHashAlgorithm.Sha1 => SHA1.Create(), SourceHashAlgorithm.Sha256 => SHA256.Create(), + SourceHashAlgorithm.Sha384 => SHA384.Create(), + SourceHashAlgorithm.Sha512 => SHA512.Create(), _ => throw ExceptionUtilities.UnexpectedValue(algorithm) }; } @@ -61,5 +71,35 @@ public static HashAlgorithm CreateDefaultInstance() { return CreateInstance(Default); } + + public static bool TryParseAlgorithmName(string name, out SourceHashAlgorithm algorithm) + { + if (string.Equals("sha1", name, StringComparison.OrdinalIgnoreCase)) + { + algorithm = SourceHashAlgorithm.Sha1; + return true; + } + + if (string.Equals("sha256", name, StringComparison.OrdinalIgnoreCase)) + { + algorithm = SourceHashAlgorithm.Sha256; + return true; + } + + if (string.Equals("sha384", name, StringComparison.OrdinalIgnoreCase)) + { + algorithm = SourceHashAlgorithm.Sha384; + return true; + } + + if (string.Equals("sha512", name, StringComparison.OrdinalIgnoreCase)) + { + algorithm = SourceHashAlgorithm.Sha512; + return true; + } + + algorithm = SourceHashAlgorithm.None; + return false; + } } } diff --git a/src/Compilers/Core/Portable/Text/SourceText.cs b/src/Compilers/Core/Portable/Text/SourceText.cs index 4a7ca58e71ad..94a5762127a0 100644 --- a/src/Compilers/Core/Portable/Text/SourceText.cs +++ b/src/Compilers/Core/Portable/Text/SourceText.cs @@ -1321,7 +1321,11 @@ internal static int GetMaxCharCountOrThrowIfHuge(Encoding encoding, Stream strea throw new IOException(CodeAnalysisResources.StreamIsTooLong); } - internal SourceText WithChecksumAlgorithm(SourceHashAlgorithm checksumAlgorithm) + /// + /// If is , returns this instance without modification. + /// Otherwise, returns a with the same as , potentially by wrapping this instance. + /// + internal SourceText WithChecksumAlgorithmIfAny(SourceHashAlgorithm checksumAlgorithm) { if (checksumAlgorithm == SourceHashAlgorithm.None || checksumAlgorithm == ChecksumAlgorithm) return this; diff --git a/src/Compilers/Core/Portable/WellKnownMember.cs b/src/Compilers/Core/Portable/WellKnownMember.cs index 24f922b946d3..73cf7662c052 100644 --- a/src/Compilers/Core/Portable/WellKnownMember.cs +++ b/src/Compilers/Core/Portable/WellKnownMember.cs @@ -571,6 +571,9 @@ internal enum WellKnownMember System_Diagnostics_CodeAnalysis_SetsRequiredMembersAttribute__ctor, System_Runtime_CompilerServices_ScopedRefAttribute__ctor, System_Runtime_CompilerServices_RefSafetyRulesAttribute__ctor, + + System_Runtime_CompilerServices_MemorySafetyRulesAttribute__ctor, + System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor, System_Runtime_CompilerServices_ClosedAttribute__ctor, System_MemoryExtensions__SequenceEqual_Span_T, @@ -721,6 +724,15 @@ internal enum WellKnownMember System_Text_Encoding__get_UTF8, System_Text_Encoding__GetString, + System_Span_T__Slice_Int, + System_ReadOnlySpan_T__Slice_Int, + System_Memory_T__Slice_Int_Int, + System_Memory_T__Slice_Int, + System_ReadOnlyMemory_T__Slice_Int_Int, + System_ReadOnlyMemory_T__Slice_Int, + + System_Runtime_CompilerServices_UnionAttribute__ctor, + Count, // Remember to update the AllWellKnownTypeMembers tests when making changes here diff --git a/src/Compilers/Core/Portable/WellKnownMembers.cs b/src/Compilers/Core/Portable/WellKnownMembers.cs index 53bd25c62ec1..ca9c815f88c7 100644 --- a/src/Compilers/Core/Portable/WellKnownMembers.cs +++ b/src/Compilers/Core/Portable/WellKnownMembers.cs @@ -3958,11 +3958,26 @@ static WellKnownMembers() // System_Runtime_CompilerServices_RefSafetyRulesAttribute__ctor (byte)MemberFlags.Constructor, // Flags (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Runtime_CompilerServices_RefSafetyRulesAttribute - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 1, // Method Signature + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Void, // Return Type + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + + // System_Runtime_CompilerServices_MemorySafetyRulesAttribute__ctor + (byte)MemberFlags.Constructor, // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Runtime_CompilerServices_MemorySafetyRulesAttribute - WellKnownType.ExtSentinel), // DeclaringTypeId 0, // Arity 1, // Method Signature (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Void, // Return Type (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + // System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor + (byte)(MemberFlags.Constructor), // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 0, // Method Signature + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Void, // Return Type + // System_Runtime_CompilerServices_ClosedAttribute__ctor (byte)MemberFlags.Constructor, // Flags (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Runtime_CompilerServices_ClosedAttribute - WellKnownType.ExtSentinel), // DeclaringTypeId @@ -5219,6 +5234,81 @@ static WellKnownMembers() (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_String, // Return Type (byte)SignatureTypeCode.Pointer, (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Byte, // Argument: byte* (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, // Argument: int + + // System_Span_T__Slice_Int + (byte)(MemberFlags.Method), // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Span_T - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 1, // Method Signature + (byte)SignatureTypeCode.GenericTypeInstance, (byte)SignatureTypeCode.TypeHandle, + (byte)WellKnownType.ExtSentinel, (WellKnownType.System_Span_T - WellKnownType.ExtSentinel), + 1, + (byte)SignatureTypeCode.GenericTypeParameter, (byte)0, // Return Type + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + + // System_ReadOnlySpan_T__Slice_Int + (byte)(MemberFlags.Method), // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_ReadOnlySpan_T - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 1, // Method Signature + (byte)SignatureTypeCode.GenericTypeInstance, (byte)SignatureTypeCode.TypeHandle, + (byte)WellKnownType.ExtSentinel, (WellKnownType.System_ReadOnlySpan_T - WellKnownType.ExtSentinel), + 1, + (byte)SignatureTypeCode.GenericTypeParameter, (byte)0, // Return Type + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + + // System_Memory_T__Slice_Int_Int + (byte)(MemberFlags.Method), // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Memory_T - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 2, // Method Signature + (byte)SignatureTypeCode.GenericTypeInstance, (byte)SignatureTypeCode.TypeHandle, + (byte)WellKnownType.ExtSentinel, (WellKnownType.System_Memory_T - WellKnownType.ExtSentinel), + 1, + (byte)SignatureTypeCode.GenericTypeParameter, (byte)0, // Return Type + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + + // System_Memory_T__Slice_Int + (byte)(MemberFlags.Method), // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Memory_T - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 1, // Method Signature + (byte)SignatureTypeCode.GenericTypeInstance, (byte)SignatureTypeCode.TypeHandle, + (byte)WellKnownType.ExtSentinel, (WellKnownType.System_Memory_T - WellKnownType.ExtSentinel), + 1, + (byte)SignatureTypeCode.GenericTypeParameter, (byte)0, // Return Type + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + + // System_ReadOnlyMemory_T__Slice_Int_Int + (byte)(MemberFlags.Method), // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_ReadOnlyMemory_T - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 2, // Method Signature + (byte)SignatureTypeCode.GenericTypeInstance, (byte)SignatureTypeCode.TypeHandle, + (byte)WellKnownType.ExtSentinel, (WellKnownType.System_ReadOnlyMemory_T - WellKnownType.ExtSentinel), + 1, + (byte)SignatureTypeCode.GenericTypeParameter, (byte)0, // Return Type + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + + // System_ReadOnlyMemory_T__Slice_Int + (byte)(MemberFlags.Method), // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_ReadOnlyMemory_T - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 1, // Method Signature + (byte)SignatureTypeCode.GenericTypeInstance, (byte)SignatureTypeCode.TypeHandle, + (byte)WellKnownType.ExtSentinel, (WellKnownType.System_ReadOnlyMemory_T - WellKnownType.ExtSentinel), + 1, + (byte)SignatureTypeCode.GenericTypeParameter, (byte)0, // Return Type + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Int32, + + // System_Runtime_CompilerServices_UnionAttribute__ctor + (byte)MemberFlags.Constructor, // Flags + (byte)WellKnownType.ExtSentinel, (byte)(WellKnownType.System_Runtime_CompilerServices_UnionAttribute - WellKnownType.ExtSentinel), // DeclaringTypeId + 0, // Arity + 0, // Method Signature + (byte)SignatureTypeCode.TypeHandle, (byte)SpecialType.System_Void, // Return Type }; string[] allNames = new string[(int)WellKnownMember.Count] @@ -5711,6 +5801,8 @@ static WellKnownMembers() ".ctor", // System_Diagnostics_CodeAnalysis_SetsRequiredMembersAttribute__ctor ".ctor", // System_Runtime_CompilerServices_ScopedRefAttribute__ctor ".ctor", // System_Runtime_CompilerServices_RefSafetyRulesAttribute__ctor + ".ctor", // System_Runtime_CompilerServices_MemorySafetyRulesAttribute__ctor + ".ctor", // System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor ".ctor", // System_Runtime_CompilerServices_ClosedAttribute__ctor "SequenceEqual", // System_MemoryExtensions__SequenceEqual_Span_T "SequenceEqual", // System_MemoryExtensions__SequenceEqual_ReadOnlySpan_T @@ -5848,6 +5940,14 @@ static WellKnownMembers() "Power", // System_Linq_Expressions_Expression__Power_MethodInfo, "get_UTF8", // System_Text_Encoding__get_UTF8 "GetString", // System_Text_Encoding__GetString + "Slice", // System_Span_T__Slice_Int + "Slice", // System_ReadOnlySpan_T__Slice_Int + "Slice", // System_Memory_T__Slice_Int_Int + "Slice", // System_Memory_T__Slice_Int + "Slice", // System_ReadOnlyMemory_T__Slice_Int_Int + "Slice", // System_ReadOnlyMemory_T__Slice_Int + + ".ctor", // System_Runtime_CompilerServices_UnionAttribute__ctor }; s_descriptors = MemberDescriptor.InitializeFromStream(new System.IO.MemoryStream(initializationBytes, writable: false), allNames); diff --git a/src/Compilers/Core/Portable/WellKnownTypes.cs b/src/Compilers/Core/Portable/WellKnownTypes.cs index ebbdd7a6c739..9387e16d3a5a 100644 --- a/src/Compilers/Core/Portable/WellKnownTypes.cs +++ b/src/Compilers/Core/Portable/WellKnownTypes.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -12,7 +12,6 @@ namespace Microsoft.CodeAnalysis /// Ids of well known runtime types. /// Values should not intersect with SpecialType enum! /// - /// internal enum WellKnownType { // Value 0 represents an unknown type @@ -319,6 +318,9 @@ internal enum WellKnownType System_Runtime_CompilerServices_DefaultInterpolatedStringHandler, System_Runtime_CompilerServices_ScopedRefAttribute, System_Runtime_CompilerServices_RefSafetyRulesAttribute, + + System_Runtime_CompilerServices_MemorySafetyRulesAttribute, + System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute, System_Runtime_CompilerServices_ClosedAttribute, System_ArgumentNullException, @@ -377,6 +379,11 @@ internal enum WellKnownType System_Runtime_CompilerServices_InlineArray15, System_Runtime_CompilerServices_InlineArray16, + System_Memory_T, + System_ReadOnlyMemory_T, + System_Runtime_CompilerServices_UnionAttribute, + System_Runtime_CompilerServices_IUnion, + NextAvailable, // Remember to update MissingSpecialMember.AllWellKnownTypes and WellKnownTypeValidationTests.AllWellKnownTypes tests when making changes here } @@ -687,7 +694,12 @@ internal static class WellKnownTypes "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler", "System.Runtime.CompilerServices.ScopedRefAttribute", "System.Runtime.CompilerServices.RefSafetyRulesAttribute", + + "System.Runtime.CompilerServices.MemorySafetyRulesAttribute", + "System.Diagnostics.CodeAnalysis.RequiresUnsafeAttribute", + "System.Runtime.CompilerServices.ClosedAttribute", + "System.ArgumentNullException", "System.Runtime.CompilerServices.RequiredMemberAttribute", @@ -739,6 +751,11 @@ internal static class WellKnownTypes "System.Runtime.CompilerServices.InlineArray14`1", "System.Runtime.CompilerServices.InlineArray15`1", "System.Runtime.CompilerServices.InlineArray16`1", + + "System.Memory`1", + "System.ReadOnlyMemory`1", + "System.Runtime.CompilerServices.UnionAttribute", + "System.Runtime.CompilerServices.IUnion", }; private static readonly Dictionary s_nameToTypeIdMap = new Dictionary((int)Count); diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.cs.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.cs.xlf index 620ac8f0896b..55d009f3ad17 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.cs.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.cs.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + Všechny analyzátory mají povolené paralelní provádění. + + The assembly containing type '{0}' references .NET Framework, which is not supported. Sestavení, které obsahuje typ {0}, se odkazuje na architekturu .NET Framework, což se nepodporuje. @@ -10,7 +15,7 @@ attribute atribut - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ Modul zahrnuje neplatné atributy. + + Analyzers that have not enabled concurrent execution: + Analyzátory, které nemají povolené paralelní provádění: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. Neohlášená diagnostika s ID {0} se nedá potlačit. @@ -273,6 +283,11 @@ SuppressionDescriptor musí mít ID, které není null, prázdný řetězec ani řetězec obsahující jenom prázdné znaky. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + POZNÁMKA: Byl nalezen tento počet potlačovačů diagnostiky: {0}. Potlačovače diagnostiky nikdy neběží paralelně. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. Nejde vygenerovat nativní soubor PDB pro '{0}' metody, protože jeho velikost metadat ladění {1} překračuje limit {2}. @@ -306,7 +321,7 @@ class třída - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate delegát - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event událost - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface rozhraní - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return návrat - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.de.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.de.xlf index ba8175d9a796..26861e572ae0 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.de.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.de.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + Alle Analyzer haben die gleichzeitige Ausführung aktiviert. + + The assembly containing type '{0}' references .NET Framework, which is not supported. Die Assembly mit dem Typ "{0}" verweist auf das .NET Framework. Dies wird nicht unterstützt. @@ -10,7 +15,7 @@ attribute Attribut - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ Das Modul weist ungültige Attribute auf. + + Analyzers that have not enabled concurrent execution: + Analysetools, die keine gleichzeitige Ausführung aktiviert haben: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. Die nicht gemeldete Diagnose mit der ID "{0}" kann nicht unterdrückt werden. @@ -273,6 +283,11 @@ Ein SuppressionDescriptor muss eine ID aufweisen, die weder NULL noch eine leere Zeichenfolge noch eine Zeichenfolge ist, die nur Leerzeichen enthält. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + HINWEIS: es wurden {0} Diagnoseunterdrücker gefunden. Diagnoseunterdrücker werden nie gleichzeitig ausgeführt. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. Native PDB für Methode '{0}' kann nicht ausgeben werden, da die Größe der Debugmetadaten {1} den Grenzwert {2} überschreitet. @@ -306,7 +321,7 @@ class Klasse - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate Delegat - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event Ereignis - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface Schnittstelle - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return Zurück - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.es.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.es.xlf index 26ea106f43a7..d65d727523e7 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.es.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.es.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + Todos los analizadores tienen habilitada la ejecución en paralelo. + + The assembly containing type '{0}' references .NET Framework, which is not supported. El ensamblado que contiene el tipo "{0}" hace referencia a .NET Framework, lo cual no se admite. @@ -10,7 +15,7 @@ attribute atributo - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ El módulo tiene atributos no válidos. + + Analyzers that have not enabled concurrent execution: + Analizadores que no han habilitado la ejecución en paralelo: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. No se puede suprimir el diagnóstico no notificado con el id. "{0}". @@ -273,6 +283,11 @@ Un elemento SuppressionDescriptor debe tener un id. que no sea NULL, una cadena vacía ni una cadena que solo contenga un espacio en blanco. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + NOTA: se encontraron {0} supresores de diagnóstico. Los supresores de diagnóstico nunca se ejecutan simultáneamente. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. No se puede emitir un archivo PDB nativo para el método '{0}' porque su tamaño de metadatos de depuración {1} supera el límite {2}. @@ -306,7 +321,7 @@ class clase - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate delegado - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event evento - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface interfaz - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return volver - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.fr.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.fr.xlf index f102bcbf6e35..fe0f27ce44c0 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.fr.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.fr.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + Tous les analyseurs ont activé l’exécution concurrente. + + The assembly containing type '{0}' references .NET Framework, which is not supported. L'assembly contenant le type '{0}' référence le .NET Framework, ce qui n'est pas pris en charge. @@ -10,7 +15,7 @@ attribute attribut - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ Le module a des attributs non valides. + + Analyzers that have not enabled concurrent execution: + Analyseurs qui n’ont pas activé l’exécution simultanée : + + Non-reported diagnostic with ID '{0}' cannot be suppressed. Impossible de supprimer le diagnostic non signalé ayant l'ID '{0}'. @@ -273,6 +283,11 @@ SuppressionDescriptor doit avoir un ID qui n'est ni une valeur null, ni une chaîne vide, ni une chaîne contenant uniquement des espaces blancs. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + REMARQUE : {0} suppresseurs de diagnostic ont été trouvés. Les suppresseurs de diagnostic ne s’exécutent jamais simultanément. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. Impossible d’émettre un fichier PDB natif pour la méthode '{0}', car sa taille des métadonnées de débogage {1} dépasse la limite {2}. @@ -306,7 +321,7 @@ class classe - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate délégué - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event événement - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface interface - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return retour - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.it.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.it.xlf index bb9ba5071374..5c09c86e8906 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.it.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.it.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + In tutti gli analizzatori è abilitata l'esecuzione parallela. + + The assembly containing type '{0}' references .NET Framework, which is not supported. L'assembly che contiene il tipo '{0}' fa riferimento a .NET Framework, che non è supportato. @@ -10,7 +15,7 @@ attribute attributo - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ Il modulo contiene attributi non validi. + + Analyzers that have not enabled concurrent execution: + Analizzatori in cui non è abilitata l'esecuzione parallela: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. Non è possibile eliminare la diagnostica non restituita con ID '{0}'. @@ -273,6 +283,11 @@ L'ID di un elemento SuppressionDescriptor non deve essere Null, né una stringa vuota o una stringa composta solo da spazi vuoti. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + NOTA: sono stati trovati {0} elementi di soppressione diagnostica. Gli elementi di soppressione diagnostica non vengono mai eseguiti parallelamente. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. Non è possibile creare il PDB nativo per il metodo '{0}' perché le dimensioni dei metadati di debug {1} superano il limite {2}. @@ -306,7 +321,7 @@ class classe - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate delegato - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event evento - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface interfaccia - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return valore restituito - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ja.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ja.xlf index e0aea3189d80..f59ded0ad308 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ja.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ja.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + すべてのアナライザーで並列実行が有効になっています。 + + The assembly containing type '{0}' references .NET Framework, which is not supported. 型 '{0}' を含むアセンブリが .NET Framework を参照しています。これはサポートされていません。 @@ -10,7 +15,7 @@ attribute 属性 - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ モジュールに無効な属性があります。 + + Analyzers that have not enabled concurrent execution: + 並列実行が有効になっていないアナライザー: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. ID '{0}' を持つ未報告の診断を抑制することはできません。 @@ -273,6 +283,11 @@ SuppressionDescriptor では、null でも、空の文字列でも、空白のみの文字列でもない ID が必要です。 + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + メモ: {0} 個の診断サプレッサーが見つかりました。診断サプレッサーは同時に実行されることはありません。 + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. メソッド '{0}' のネイティブ PDB を生成できません。デバッグ メタデータ のサイズ {1} が制限 {2} を超えています。 @@ -306,7 +321,7 @@ class クラス - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate デリゲート - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event イベント - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface インターフェイス - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -365,8 +380,8 @@ return - 戻る - + 戻り値 + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ko.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ko.xlf index d041f8bf40b7..58a0ddbcaa1b 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ko.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ko.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + 모든 분석기가 병렬 실행을 활성화했습니다. + + The assembly containing type '{0}' references .NET Framework, which is not supported. '{0}' 형식을 포함하는 어셈블리가 지원되지 않는 .NET Framework를 참조합니다. @@ -10,7 +15,7 @@ attribute 특성 - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ 모듈에 잘못된 특성이 있습니다. + + Analyzers that have not enabled concurrent execution: + 동시 실행을 활성화하지 않은 분석기: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. ID가 '{0}'인 보고되지 않는 진단은 표시되지 않도록 설정할 수 없습니다. @@ -273,6 +283,11 @@ SuppressionDescriptor에 null, 빈 문자열 및 공백만 포함된 문자열이 아닌 ID가 있어야 합니다. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + 참고: {0}개의 진단 억제기가 발견되었습니다. 진단 억제기는 동시에 실행되지 않습니다. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. 디버그 메타데이터 크기 {1} 제한 {2} 초과하여 메서드 '{0}' 네이티브 PDB를 내보낼 수 없습니다. @@ -306,7 +321,7 @@ class 클래스 - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate 대리자 - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event 이벤트 - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface 인터페이스 - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return 돌아가기 - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pl.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pl.xlf index 12e70ce3d9fa..294ce5508247 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pl.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pl.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + Wszystkie analizatory mają włączone wykonywanie równoległe. + + The assembly containing type '{0}' references .NET Framework, which is not supported. Zestaw zawierający typ „{0}” odwołuje się do platformy .NET Framework, co nie jest obsługiwane. @@ -10,7 +15,7 @@ attribute atrybut - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ Moduł ma nieprawidłowe atrybuty. + + Analyzers that have not enabled concurrent execution: + Analizatory, które nie mają włączonego wykonywania równoległego: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. Nie można pominąć niezgłoszonej diagnostyki o identyfikatorze „{0}”. @@ -273,6 +283,11 @@ Interfejs DiagnosticDescriptor musi mieć identyfikator, który nie ma wartości null, nie jest ciągiem pustym ani nie jest ciągiem zawierającym tylko białe znaki. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + UWAGA: znaleziono następującą liczbę wyłączników diagnostycznych: {0}. Wyłączniki diagnostyczne nigdy nie działają jednocześnie. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. Nie można wyemitować natywnego pliku PDB dla metody '{0}', ponieważ jego rozmiar metadanych debugowania {1} przekracza limit {2}. @@ -306,7 +321,7 @@ class klasa - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate delegat - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event zdarzenie - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface interfejs - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return zwracany - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pt-BR.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pt-BR.xlf index 6ed4ccf4d987..7d8644b4e7fc 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pt-BR.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.pt-BR.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + Todos os analisadores habilitaram a execução paralela. + + The assembly containing type '{0}' references .NET Framework, which is not supported. O assembly contendo o tipo '{0}' faz referência a .NET Framework, mas não há suporte para isso. @@ -10,7 +15,7 @@ attribute atributo - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ O módulo tem atributos inválidos. + + Analyzers that have not enabled concurrent execution: + Analisadores que não habilitam a execução paralela: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. O diagnóstico não relatado com a ID '{0}' não pode ser suprimido. @@ -273,6 +283,11 @@ A ID do SuppressionDescriptor não pode ser nula, não deve ser uma cadeia de caracteres vazia nem deve ser uma cadeia de caracteres que contenha apenas espaços em branco. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + OBSERVAÇÃO: foram encontrados {0} supressores de diagnóstico. Os supressores de diagnóstico nunca são executados simultaneamente. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. Não é possível emitir PDB nativo para o método '{0}' porque seu tamanho de metadados de depuração {1} está acima do limite {2}. @@ -306,7 +321,7 @@ class classe - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate delegar - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event evento - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface interface - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return retornar - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ru.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ru.xlf index af8119ca30c3..3b28ad5e0cea 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ru.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.ru.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + Для всех анализаторов включено параллельное выполнение. + + The assembly containing type '{0}' references .NET Framework, which is not supported. Сборка, содержащая тип "{0}", ссылается на платформу .NET Framework, которая не поддерживается. @@ -10,7 +15,7 @@ attribute атрибут - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ Модуль содержит недопустимые атрибуты. + + Analyzers that have not enabled concurrent execution: + Анализаторы, для которых не включено параллельное выполнение: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. Невозможно подавить невыводимую диагностику с идентификатором "{0}". @@ -273,6 +283,11 @@ SuppressionDescriptor должен иметь идентификатор, который не является значением NULL, пустой строкой или строкой, состоящей из одного пробела. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + ПРИМЕЧАНИЕ. Обнаружены средства подавления диагностики: {0}. Средства подавления диагностики никогда не выполняются одновременно. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. Не удается создать собственный PDB-файл для '{0}', так как размер метаданных отладки {1} превышает ограничение {2}. @@ -306,7 +321,7 @@ class класс - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate делегат - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event событие - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface интерфейс - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return возврат - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.tr.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.tr.xlf index 33e908472df8..8800f1d569cd 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.tr.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.tr.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + Tüm çözümleyiciler paralel yürütmeyi etkinleştirdi. + + The assembly containing type '{0}' references .NET Framework, which is not supported. '{0}' türünü içeren bütünleştirilmiş kod, desteklenmeyen .NET Framework'e başvuruyor. @@ -10,7 +15,7 @@ attribute öznitelik - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ Modülde geçersiz öznitelikler var. + + Analyzers that have not enabled concurrent execution: + Paralel yürütmeyi etkinleştirmemiş çözümleyiciler: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. '{0}' kimlikli raporlanamayan tanılama gizlenemiyor. @@ -273,6 +283,11 @@ SuppressionDescriptor türünün kimliği, null veya boş bir dize ya da yalnızca boşluk içeren bir dize olmamalıdır. + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + NOT: {0} tanılama koruyucusu bulundu. Tanılama koruyucuları hiçbir zaman paralel çalışmaz. + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. Hata ayıklama meta veri boyutu '{0}' sınırı aşıldığından yöntem {1} için yerel PDB {2}. @@ -306,7 +321,7 @@ class sınıf - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate temsilci - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event olay - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface arabirim - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return dönüş - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hans.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hans.xlf index 0ff9e7eff8c9..d80996726e23 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hans.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hans.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + 所有分析器均已启用并行执行。 + + The assembly containing type '{0}' references .NET Framework, which is not supported. 包含类型“{0}”的程序集引用了 .NET Framework,而此操作不受支持。 @@ -10,7 +15,7 @@ attribute 属性 - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ 模块具有无效属性。 + + Analyzers that have not enabled concurrent execution: + 未启用并行执行的分析器: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. 不可禁止显示 ID 为“{0}”的未报告的诊断。 @@ -273,6 +283,11 @@ A SuppressionDescriptor 必须具有一个 ID,且该 ID 不为 null、不是空字符串且不是仅包含空格的字符串。 + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + 注意: 找到 {0} 个诊断抑制器。诊断抑制器从不并行运行。 + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. 无法发出方法 '{0}' 的本机 PDB,因为其调试元数据大小 {1} 超过 {2} 限制。 @@ -306,7 +321,7 @@ class - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate 委托 - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event 事件 - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface 接口 - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return 返回 - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hant.xlf b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hant.xlf index 9b3d0ab0ed62..75e76da9c895 100644 --- a/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hant.xlf +++ b/src/Compilers/Core/Portable/xlf/CodeAnalysisResources.zh-Hant.xlf @@ -2,6 +2,11 @@ + + All analyzers have enabled concurrent execution. + 所有分析程式均已啟用平行執行。 + + The assembly containing type '{0}' references .NET Framework, which is not supported. 包含類型 '{0}' 的組件參考了 .NET Framework,此情形不受支援。 @@ -10,7 +15,7 @@ attribute 屬性 - + Refers to a programming attribute/annotation (noun), not the verb "to attribute" Illegal built-in operator name '{0}' @@ -183,6 +188,11 @@ 模組有無效的屬性。 + + Analyzers that have not enabled concurrent execution: + 未啟用平行執行的分析程式: + + Non-reported diagnostic with ID '{0}' cannot be suppressed. 無法隱藏識別碼為 '{0}' 的非回報診斷。 @@ -273,6 +283,11 @@ SuppressionDescriptor 的識別碼不得為 null、空白字串或只包含空白字元的字串。 + + NOTE: {0} diagnostic suppressors were found. Diagnostic suppressors never run concurrently. + 注意: 找到 {0} 個診斷抑制程式。診斷抑制程式不會同時執行。 + + Cannot emit native PDB for method '{0}' because its debug metadata size {1} is over the limit {2}. 無法發出方法 '{0}' 的原生 PDB,因為其偵錯元數據大小 {1} 超過限制 {2}。 @@ -306,7 +321,7 @@ class 類別 - + Refers to a class type in programming (noun), not the verb "to classify" constructor @@ -316,7 +331,7 @@ delegate 委派 - + Refers to a delegate type in programming (noun), not the verb "to delegate" enum @@ -326,7 +341,7 @@ event 事件 - + Refers to a programming event member (noun), not "to occur" or other verb meanings field @@ -341,7 +356,7 @@ interface 介面 - + Refers to an interface type in programming (noun), not the verb "to interface" method @@ -366,7 +381,7 @@ return 返回 - + Refers to a method return value/type (noun), not the verb "to return" struct diff --git a/src/Compilers/Core/Rebuild/CompilationOptionsReader.cs b/src/Compilers/Core/Rebuild/CompilationOptionsReader.cs index 59fefd67f495..1edd499129c5 100644 --- a/src/Compilers/Core/Rebuild/CompilationOptionsReader.cs +++ b/src/Compilers/Core/Rebuild/CompilationOptionsReader.cs @@ -28,6 +28,8 @@ public class CompilationOptionsReader // GUIDs specified in https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#document-table-0x30 public static readonly Guid HashAlgorithmSha1 = unchecked(new Guid((int)0xff1816ec, (short)0xaa5e, 0x4d10, 0x87, 0xf7, 0x6f, 0x49, 0x63, 0x83, 0x34, 0x60)); public static readonly Guid HashAlgorithmSha256 = unchecked(new Guid((int)0x8829d00f, 0x11b8, 0x4213, 0x87, 0x8b, 0x77, 0x0e, 0x85, 0x97, 0xac, 0x16)); + public static readonly Guid HashAlgorithmSha384 = unchecked(new Guid((int)0xd99cfeb1, (short)0x8c43, 0x444a, 0x8a, 0x6c, 0xb6, 0x12, 0x69, 0xd2, 0xa0, 0xbf)); + public static readonly Guid HashAlgorithmSha512 = unchecked(new Guid((int)0xef2d1afc, 0x6550, 0x46d6, 0xb1, 0x4b, 0xd7, 0x0a, 0xfe, 0x9a, 0x55, 0x66)); // https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#compilation-metadata-references-c-and-vb-compilers public static readonly Guid MetadataReferenceInfoGuid = new Guid("7E4D4708-096E-4C5C-AEDA-CB10BA6A740D"); @@ -187,6 +189,8 @@ public IEnumerable GetEmbeddedSourceTextInfo() var hashAlgorithm = hashAlgorithmGuid == HashAlgorithmSha1 ? SourceHashAlgorithm.Sha1 : hashAlgorithmGuid == HashAlgorithmSha256 ? SourceHashAlgorithm.Sha256 + : hashAlgorithmGuid == HashAlgorithmSha384 ? SourceHashAlgorithm.Sha384 + : hashAlgorithmGuid == HashAlgorithmSha512 ? SourceHashAlgorithm.Sha512 : SourceHashAlgorithm.None; var hash = PdbReader.GetBlobBytes(document.Hash); diff --git a/src/Compilers/Core/RebuildTest/BasicDeterministicKeyBuilderTests.cs b/src/Compilers/Core/RebuildTest/BasicDeterministicKeyBuilderTests.cs index eb19c8972aef..0a9ea71b00d6 100644 --- a/src/Compilers/Core/RebuildTest/BasicDeterministicKeyBuilderTests.cs +++ b/src/Compilers/Core/RebuildTest/BasicDeterministicKeyBuilderTests.cs @@ -210,19 +210,7 @@ public void BasicPathMapWindows(string filePath, string workingDirectory, string ""checksumAlgorithm"": ""Sha256"", ""encodingName"": ""Unicode (UTF-8)"" }, - ""parseOptions"": { - ""kind"": ""Regular"", - ""specifiedKind"": ""Regular"", - ""documentationMode"": ""None"", - ""language"": ""Visual Basic"", - ""features"": {}, - ""languageVersion"": ""VisualBasic15"", - ""specifiedLanguageVersion"": ""VisualBasic15"", - ""preprocessorSymbols"": { - ""TARGET"": ""exe"", - ""VBC_VER"": ""17.13"" - } - } + ""parseOptionsIndex"": 0 } ] ", compiler); @@ -274,6 +262,20 @@ public void MetadataReferenceCompilation() ""globalImports"": [], ""parseOptions"": null }, + ""parseOptions"": [ + { + ""kind"": ""Regular"", + ""specifiedKind"": ""Regular"", + ""documentationMode"": ""Parse"", + ""language"": ""Visual Basic"", + ""features"": {}, + ""languageVersion"": ""VisualBasic15"", + ""specifiedLanguageVersion"": ""VisualBasic15"", + ""preprocessorSymbols"": { + ""_MYTYPE"": ""Empty"" + } + } + ], ""syntaxTrees"": [ { ""fileName"": """", @@ -282,18 +284,7 @@ public void MetadataReferenceCompilation() ""checksumAlgorithm"": ""Sha1"", ""encodingName"": null }, - ""parseOptions"": { - ""kind"": ""Regular"", - ""specifiedKind"": ""Regular"", - ""documentationMode"": ""Parse"", - ""language"": ""Visual Basic"", - ""features"": {}, - ""languageVersion"": ""VisualBasic15"", - ""specifiedLanguageVersion"": ""VisualBasic15"", - ""preprocessorSymbols"": { - ""_MYTYPE"": ""Empty"" - } - } + ""parseOptionsIndex"": 0 } ] } @@ -366,6 +357,24 @@ public void FeatureFlag() }} }} }}, + ""parseOptions"": [ + {{ + ""kind"": ""Regular"", + ""specifiedKind"": ""Regular"", + ""documentationMode"": ""None"", + ""language"": ""Visual Basic"", + ""features"": {{ + ""debug-determinism"": ""true"" + }}, + ""languageVersion"": ""VisualBasic17_13"", + ""specifiedLanguageVersion"": ""Default"", + ""preprocessorSymbols"": {{ + ""TARGET"": ""library"", + ""VBC_VER"": ""17.13"", + ""_MYTYPE"": ""Empty"" + }} + }} + ], ""syntaxTrees"": [ {{ ""fileName"": ""{Roslyn.Utilities.JsonWriter.EscapeString(sourceFile.FilePath)}"", @@ -374,22 +383,7 @@ public void FeatureFlag() ""checksumAlgorithm"": ""Sha256"", ""encodingName"": ""Unicode (UTF-8)"" }}, - ""parseOptions"": {{ - ""kind"": ""Regular"", - ""specifiedKind"": ""Regular"", - ""documentationMode"": ""None"", - ""language"": ""Visual Basic"", - ""features"": {{ - ""debug-determinism"": ""true"" - }}, - ""languageVersion"": ""VisualBasic17_13"", - ""specifiedLanguageVersion"": ""Default"", - ""preprocessorSymbols"": {{ - ""TARGET"": ""library"", - ""VBC_VER"": ""17.13"", - ""_MYTYPE"": ""Empty"" - }} - }} + ""parseOptionsIndex"": 0 }} ] }}, @@ -422,5 +416,35 @@ public void FeatureFlag() "; AssertJson(expected, json, "toolsVersions", "references", "extensions"); } + + [Fact] + public void ParseOptionsDeduplication() + { + // Trees with different ParseOptions produce multiple entries in the parseOptions array; + // trees with the same options share an index. + var options1 = VisualBasicParseOptions.Default.WithPreprocessorSymbols(new KeyValuePair("DEBUG", true)); + var options2 = VisualBasicParseOptions.Default.WithPreprocessorSymbols(new KeyValuePair("RELEASE", true)); + + var tree1 = VisualBasicSyntaxTree.ParseText("Class A\nEnd Class", path: "a.vb", options: options1); + var tree2 = VisualBasicSyntaxTree.ParseText("Class B\nEnd Class", path: "b.vb", options: options2); + var tree3 = VisualBasicSyntaxTree.ParseText("Class C\nEnd Class", path: "c.vb", options: options1); + + var compilation = VisualBasicCompilation.Create( + "test", + new[] { tree1, tree2, tree3 }, + NetCoreApp.References.ToArray(), + options: BasicOptions); + var key = compilation.GetDeterministicKey(options: DeterministicKeyOptions.IgnoreToolVersions); + + // Two distinct ParseOptions → two entries in the parseOptions array. + var parseOptionsArray = (Newtonsoft.Json.Linq.JArray)GetJsonProperty(key, "compilation.parseOptions").Value; + Assert.Equal(2, parseOptionsArray.Count); + + // tree1 and tree3 share options1 (index 0); tree2 has options2 (index 1). + var syntaxTreesArray = (Newtonsoft.Json.Linq.JArray)GetJsonProperty(key, "compilation.syntaxTrees").Value; + Assert.Equal(0, syntaxTreesArray[0].Value("parseOptionsIndex")); + Assert.Equal(1, syntaxTreesArray[1].Value("parseOptionsIndex")); + Assert.Equal(0, syntaxTreesArray[2].Value("parseOptionsIndex")); + } } } diff --git a/src/Compilers/Core/RebuildTest/CSharpDeterministicKeyBuilderTests.cs b/src/Compilers/Core/RebuildTest/CSharpDeterministicKeyBuilderTests.cs index 0cf662fddb49..036f176d9dbd 100644 --- a/src/Compilers/Core/RebuildTest/CSharpDeterministicKeyBuilderTests.cs +++ b/src/Compilers/Core/RebuildTest/CSharpDeterministicKeyBuilderTests.cs @@ -57,7 +57,7 @@ public void VerifyUpToDate() verifyCount(11); verifyCount(13); verifyCount(63); - verifyCount(9); + verifyCount(12); static void verifyCount(int expected) { @@ -106,9 +106,22 @@ public void Simple() ""specificDiagnosticOptions"": [], ""localtime"": null, ""unsafe"": false, + ""memorySafetyRules"": 0, ""topLevelBinderFlags"": ""None"", ""usings"": [] }, + ""parseOptions"": [ + { + ""kind"": ""Regular"", + ""specifiedKind"": ""Regular"", + ""documentationMode"": ""Parse"", + ""language"": ""C#"", + ""features"": {}, + ""languageVersion"": ""Preview"", + ""specifiedLanguageVersion"": ""Preview"", + ""preprocessorSymbols"": [] + } + ], ""syntaxTrees"": [ { ""fileName"": """", @@ -117,16 +130,7 @@ public void Simple() ""checksumAlgorithm"": ""Sha1"", ""encodingName"": ""Unicode (UTF-8)"" }, - ""parseOptions"": { - ""kind"": ""Regular"", - ""specifiedKind"": ""Regular"", - ""documentationMode"": ""Parse"", - ""language"": ""C#"", - ""features"": {}, - ""languageVersion"": ""Preview"", - ""specifiedLanguageVersion"": ""Preview"", - ""preprocessorSymbols"": [] - } + ""parseOptionsIndex"": 0 } ] }, @@ -165,16 +169,7 @@ public void SyntaxTreeFilePath(bool ignoreFilePaths) ""checksumAlgorithm"": ""Sha1"", ""encodingName"": ""Unicode (UTF-8)"" }}, - ""parseOptions"": {{ - ""kind"": ""Regular"", - ""specifiedKind"": ""Regular"", - ""documentationMode"": ""Parse"", - ""language"": ""C#"", - ""features"": {{}}, - ""languageVersion"": ""Preview"", - ""specifiedLanguageVersion"": ""Preview"", - ""preprocessorSymbols"": [] - }} + ""parseOptionsIndex"": 0 }} ]"; AssertJsonSection(expected, key, "compilation.syntaxTrees"); @@ -258,6 +253,7 @@ public void ToolsVersion() ""specificDiagnosticOptions"": [], ""localtime"": null, ""unsafe"": false, + ""memorySafetyRules"": 0, ""topLevelBinderFlags"": ""None"", ""usings"": [] }} @@ -268,22 +264,27 @@ public void ToolsVersion() ""emitOptions"": null, ""resources"": [] }} -", key, "references", "syntaxTrees", "extensions"); +", key, "references", "syntaxTrees", "extensions", "parseOptions"); } [Theory] [CombinatorialData] - public void CSharpCompilationOptionsCombination(bool @unsafe, NullableContextOptions nullableContextOptions) + public void CSharpCompilationOptionsCombination( + bool @unsafe, + [CombinatorialValues(0, 2)] int memorySafetyRules, + NullableContextOptions nullableContextOptions) { foreach (BinderFlags binderFlags in Enum.GetValues(typeof(BinderFlags))) { var options = Options .WithAllowUnsafe(@unsafe) + .WithMemorySafetyRules(memorySafetyRules) .WithTopLevelBinderFlags(binderFlags) .WithNullableContextOptions(nullableContextOptions); var value = GetCompilationOptionsValue(options); Assert.Equal(@unsafe, value.Value("unsafe")); + Assert.Equal(memorySafetyRules, value.Value("memorySafetyRules")); Assert.Equal(binderFlags.ToString(), value.Value("topLevelBinderFlags")); Assert.Equal(nullableContextOptions.ToString(), value.Value("nullableContextOptions")); } @@ -389,16 +390,7 @@ public void CSharpPathMapWindows(string filePath, string workingDirectory, strin ""checksumAlgorithm"": ""Sha256"", ""encodingName"": ""Unicode (UTF-8)"" }, - ""parseOptions"": { - ""kind"": ""Regular"", - ""specifiedKind"": ""Regular"", - ""documentationMode"": ""None"", - ""language"": ""C#"", - ""features"": {}, - ""languageVersion"": ""CSharp9"", - ""specifiedLanguageVersion"": ""CSharp9"", - ""preprocessorSymbols"": [] - } + ""parseOptionsIndex"": 0 } ] ", compiler); @@ -459,9 +451,22 @@ public void MetadataReferenceCompilation() ""specificDiagnosticOptions"": [], ""localtime"": null, ""unsafe"": false, + ""memorySafetyRules"": 0, ""topLevelBinderFlags"": ""None"", ""usings"": [] }, + ""parseOptions"": [ + { + ""kind"": ""Regular"", + ""specifiedKind"": ""Regular"", + ""documentationMode"": ""Parse"", + ""language"": ""C#"", + ""features"": {}, + ""languageVersion"": ""CSharp10"", + ""specifiedLanguageVersion"": ""CSharp10"", + ""preprocessorSymbols"": [] + } + ], ""syntaxTrees"": [ { ""fileName"": """", @@ -470,16 +475,7 @@ public void MetadataReferenceCompilation() ""checksumAlgorithm"": ""Sha1"", ""encodingName"": null }, - ""parseOptions"": { - ""kind"": ""Regular"", - ""specifiedKind"": ""Regular"", - ""documentationMode"": ""Parse"", - ""language"": ""C#"", - ""features"": {}, - ""languageVersion"": ""CSharp10"", - ""specifiedLanguageVersion"": ""CSharp10"", - ""preprocessorSymbols"": [] - } + ""parseOptionsIndex"": 0 } ] } @@ -530,9 +526,24 @@ public void FeatureFlag() "specificDiagnosticOptions": [], "localtime": null, "unsafe": false, + "memorySafetyRules": 0, "topLevelBinderFlags": "None", "usings": [] }, + "parseOptions": [ + { + "kind": "Regular", + "specifiedKind": "Regular", + "documentationMode": "None", + "language": "C#", + "features": { + "debug-determinism": "true" + }, + "languageVersion": "{{LanguageVersionFacts.CurrentVersion}}", + "specifiedLanguageVersion": "Default", + "preprocessorSymbols": [] + } + ], "syntaxTrees": [ { "fileName": "{{Roslyn.Utilities.JsonWriter.EscapeString(sourceFile.FilePath)}}", @@ -541,18 +552,7 @@ public void FeatureFlag() "checksumAlgorithm": "Sha256", "encodingName": "Unicode (UTF-8)" }, - "parseOptions": { - "kind": "Regular", - "specifiedKind": "Regular", - "documentationMode": "None", - "language": "C#", - "features": { - "debug-determinism": "true" - }, - "languageVersion": "{{LanguageVersionFacts.CurrentVersion}}", - "specifiedLanguageVersion": "Default", - "preprocessorSymbols": [] - } + "parseOptionsIndex": 0 } ] }, @@ -704,16 +704,7 @@ public void MultipleSyntaxTreesWithDifferentChecksumAlgorithms() "checksumAlgorithm": "Sha1", "encodingName": "Unicode (UTF-8)" }, - "parseOptions": { - "kind": "Regular", - "specifiedKind": "Regular", - "documentationMode": "Parse", - "language": "C#", - "features": {}, - "languageVersion": "Preview", - "specifiedLanguageVersion": "Preview", - "preprocessorSymbols": [] - } + "parseOptionsIndex": 0 }, { "fileName": "file2.cs", @@ -722,19 +713,36 @@ public void MultipleSyntaxTreesWithDifferentChecksumAlgorithms() "checksumAlgorithm": "Sha256", "encodingName": "Unicode (UTF-8)" }, - "parseOptions": { - "kind": "Regular", - "specifiedKind": "Regular", - "documentationMode": "Parse", - "language": "C#", - "features": {}, - "languageVersion": "Preview", - "specifiedLanguageVersion": "Preview", - "preprocessorSymbols": [] - } + "parseOptionsIndex": 0 } ] """, key, "compilation.syntaxTrees"); + + // Both trees share the same ParseOptions, so the parseOptions array should have exactly one entry. + var parseOptionsArray = (Newtonsoft.Json.Linq.JArray)GetJsonProperty(key, "compilation.parseOptions").Value; + Assert.Equal(1, parseOptionsArray.Count); + } + + [Fact] + public void ParseOptionsDeduplication() + { + // Trees with the same ParseOptions produce only one entry in the parseOptions array. + // Both trees share the default CSharpParseOptions, so there should be exactly one entry, + // and both trees should reference index 0. + var tree1 = CSharpTestBase.Parse("class A { }", filename: "a.cs"); + var tree2 = CSharpTestBase.Parse("class B { }", filename: "b.cs"); + + var compilation = CSharpTestBase.CreateCompilation([tree1, tree2], options: Options); + var key = compilation.GetDeterministicKey(options: DeterministicKeyOptions.IgnoreToolVersions); + + // One unique set of ParseOptions → one entry in the parseOptions array. + var parseOptionsArray = (Newtonsoft.Json.Linq.JArray)GetJsonProperty(key, "compilation.parseOptions").Value; + Assert.Equal(1, parseOptionsArray.Count); + + // Both syntax trees reference index 0. + var syntaxTreesArray = (Newtonsoft.Json.Linq.JArray)GetJsonProperty(key, "compilation.syntaxTrees").Value; + Assert.Equal(0, syntaxTreesArray[0].Value("parseOptionsIndex")); + Assert.Equal(0, syntaxTreesArray[1].Value("parseOptionsIndex")); } } } diff --git a/src/Compilers/Core/RebuildTest/DeterministicKeyBuilderTests.cs b/src/Compilers/Core/RebuildTest/DeterministicKeyBuilderTests.cs index 803e65645644..ea33a7c93da3 100644 --- a/src/Compilers/Core/RebuildTest/DeterministicKeyBuilderTests.cs +++ b/src/Compilers/Core/RebuildTest/DeterministicKeyBuilderTests.cs @@ -37,7 +37,9 @@ public abstract partial class DeterministicKeyBuilderTests("parseOptionsIndex"); + var parseOptionsArray = (JArray)GetJsonProperty(key, "compilation.parseOptions").Value; + return (JObject)parseOptionsArray[parseOptionsIndex]; } protected JArray GetReferenceValues(Compilation compilation) @@ -301,7 +306,7 @@ public void SyntaxTreeContent(string content) }} }} ]"; - AssertJsonSection(expected, key, "compilation.syntaxTrees", "parseOptions"); + AssertJsonSection(expected, key, "compilation.syntaxTrees", "parseOptionsIndex"); } } diff --git a/src/Compilers/Server/VBCSCompiler/AnyCpu/VBCSCompiler.csproj b/src/Compilers/Server/VBCSCompiler/AnyCpu/VBCSCompiler.csproj index 63820995a724..5185aede1f0a 100644 --- a/src/Compilers/Server/VBCSCompiler/AnyCpu/VBCSCompiler.csproj +++ b/src/Compilers/Server/VBCSCompiler/AnyCpu/VBCSCompiler.csproj @@ -5,7 +5,6 @@ Exe $(NetRoslynSourceBuild);net472 true - false diff --git a/src/Compilers/Server/VBCSCompiler/BuildServerController.cs b/src/Compilers/Server/VBCSCompiler/BuildServerController.cs index 0bf5c33d143b..2364bbd648a1 100644 --- a/src/Compilers/Server/VBCSCompiler/BuildServerController.cs +++ b/src/Compilers/Server/VBCSCompiler/BuildServerController.cs @@ -25,49 +25,42 @@ internal sealed class BuildServerController { internal const string KeepAliveSettingName = "keepalive"; - private readonly NameValueCollection _appSettings; private readonly ICompilerServerLogger _logger; - internal BuildServerController(NameValueCollection appSettings, ICompilerServerLogger logger) + internal BuildServerController(ICompilerServerLogger logger) { - _appSettings = appSettings; _logger = logger; } - internal int Run(string[] args) + internal int Run(string? pipeName, bool shutdown, TimeSpan? keepAlive) { - string? pipeName; - bool shutdown; - if (!ParseCommandLine(args, out pipeName, out shutdown)) - { - return CommonCompiler.Failed; - } - - pipeName = pipeName ?? GetDefaultPipeName(); - if (pipeName is null) - { - throw new Exception("Cannot calculate pipe name"); - } - var cancellationTokenSource = new CancellationTokenSource(); Console.CancelKeyPress += (sender, e) => { cancellationTokenSource.Cancel(); }; return shutdown ? RunShutdown(pipeName, cancellationToken: cancellationTokenSource.Token) - : RunServer(pipeName, cancellationToken: cancellationTokenSource.Token); + : RunServer(pipeName, keepAlive: keepAlive, cancellationToken: cancellationTokenSource.Token); } - internal TimeSpan? GetKeepAliveTimeout() + internal static TimeSpan GetDefaultKeepAlive(ICompilerServerLogger logger, NameValueCollection? appSettings = null) { try { - if (int.TryParse(_appSettings[KeepAliveSettingName], NumberStyles.Integer, CultureInfo.InvariantCulture, out int keepAliveValue) && +#if NET472 + appSettings ??= System.Configuration.ConfigurationManager.AppSettings; +#endif + if (appSettings is null) + { + return ServerDispatcher.DefaultServerKeepAlive; + } + + if (int.TryParse(appSettings[KeepAliveSettingName], NumberStyles.Integer, CultureInfo.InvariantCulture, out int keepAliveValue) && keepAliveValue >= 0) { if (keepAliveValue == 0) { // This is a one time server entry. - return null; + return Timeout.InfiniteTimeSpan; } else { @@ -81,7 +74,7 @@ internal int Run(string[] args) } catch (Exception e) { - _logger.LogException(e, "Could not read AppSettings"); + logger.LogException(e, "Could not read AppSettings"); return ServerDispatcher.DefaultServerKeepAlive; } } @@ -101,14 +94,19 @@ internal static ICompilerServerHost CreateCompilerServerHost(ICompilerServerLogg } internal int RunServer( - string pipeName, + string? pipeName = null, ICompilerServerHost? compilerServerHost = null, IClientConnectionHost? clientConnectionHost = null, IDiagnosticListener? listener = null, TimeSpan? keepAlive = null, CancellationToken cancellationToken = default) { - keepAlive ??= GetKeepAliveTimeout(); + pipeName ??= GetDefaultPipeName(); + if (pipeName is null) + { + throw new Exception("Cannot calculate pipe name"); + } + listener ??= new EmptyDiagnosticListener(); compilerServerHost ??= CreateCompilerServerHost(_logger); clientConnectionHost ??= CreateClientConnectionHost(pipeName, _logger); @@ -126,11 +124,12 @@ internal int RunServer( return CommonCompiler.Failed; } - compilerServerHost.Logger.Log("Keep alive timeout is: {0} milliseconds.", keepAlive?.TotalMilliseconds ?? 0); + keepAlive ??= GetDefaultKeepAlive(_logger); + compilerServerHost.Logger.Log("Keep alive timeout is: {0} milliseconds.", keepAlive.Value.TotalMilliseconds); FatalError.SetHandlers(FailFast.Handler, nonFatalHandler: null); var dispatcher = new ServerDispatcher(compilerServerHost, clientConnectionHost, listener); - dispatcher.ListenAndDispatchConnections(keepAlive, cancellationToken); + dispatcher.ListenAndDispatchConnections(keepAlive.Value, cancellationToken); return CommonCompiler.Succeeded; } } @@ -141,21 +140,25 @@ internal static int CreateAndRunServer( IClientConnectionHost? clientConnectionHost = null, IDiagnosticListener? listener = null, TimeSpan? keepAlive = null, - NameValueCollection? appSettings = null, ICompilerServerLogger? logger = null, CancellationToken cancellationToken = default) { - appSettings ??= new NameValueCollection(); logger ??= EmptyCompilerServerLogger.Instance; - var controller = new BuildServerController(appSettings, logger); - return controller.RunServer(pipeName, compilerServerHost, clientConnectionHost, listener, keepAlive, cancellationToken); + var controller = new BuildServerController(logger); + return controller.RunServer(pipeName, compilerServerHost, clientConnectionHost, listener, keepAlive, cancellationToken: cancellationToken); } - internal int RunShutdown(string pipeName, int? timeoutOverride = null, CancellationToken cancellationToken = default) => + internal int RunShutdown(string? pipeName, int? timeoutOverride = null, CancellationToken cancellationToken = default) => RunShutdownAsync(pipeName, waitForProcess: true, timeoutOverride, cancellationToken).GetAwaiter().GetResult(); - internal async Task RunShutdownAsync(string pipeName, bool waitForProcess, int? timeoutOverride, CancellationToken cancellationToken = default) + internal async Task RunShutdownAsync(string? pipeName, bool waitForProcess, int? timeoutOverride, CancellationToken cancellationToken = default) { + pipeName ??= GetDefaultPipeName(); + if (pipeName is null) + { + throw new Exception("Cannot calculate pipe name"); + } + var success = await BuildServerConnection.RunServerShutdownRequestAsync( pipeName, timeoutOverride, @@ -165,17 +168,55 @@ internal async Task RunShutdownAsync(string pipeName, bool waitForProcess, return success ? CommonCompiler.Succeeded : CommonCompiler.Failed; } - internal static bool ParseCommandLine(string[] args, out string? pipeName, out bool shutdown) + /// + /// Parses the command-line arguments for the build server. + /// + /// + /// Recognized options: + /// + /// -pipename:<name> — the named pipe to listen on. + /// -timeout:<seconds> — keep-alive in seconds; 0 means infinite (no timeout). + /// -log:<path> — path to the log file. + /// -shutdown — request the server to shut down. + /// + /// + internal static bool ParseCommandLine(string[] args, out string? pipeName, out bool shutdown, out TimeSpan? timeout, out string? logFilePath) { pipeName = null; shutdown = false; + timeout = null; + logFilePath = null; foreach (var arg in args) { const string pipeArgPrefix = "-pipename:"; - if (arg.StartsWith(pipeArgPrefix, StringComparison.Ordinal)) + const string timeoutArgPrefix = "-timeout:"; + const string logArgPrefix = "-log:"; + var argSpan = arg.AsSpan(); + if (argSpan.StartsWith(pipeArgPrefix.AsSpan(), StringComparison.Ordinal)) + { + pipeName = argSpan[pipeArgPrefix.Length..].ToString(); + } + else if (argSpan.StartsWith(timeoutArgPrefix.AsSpan(), StringComparison.Ordinal)) { - pipeName = arg.Substring(pipeArgPrefix.Length); + var timeoutValue = argSpan[timeoutArgPrefix.Length..]; + if (!int.TryParse(timeoutValue.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedTimeout) || + parsedTimeout < 0) + { + return false; + } + + timeout = parsedTimeout == 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(parsedTimeout); + } + else if (argSpan.StartsWith(logArgPrefix.AsSpan(), StringComparison.Ordinal)) + { + var parsedLogFilePath = argSpan[logArgPrefix.Length..]; + if (parsedLogFilePath.Length == 0) + { + return false; + } + + logFilePath = parsedLogFilePath.ToString(); } else if (arg == "-shutdown") { diff --git a/src/Compilers/Server/VBCSCompiler/ServerDispatcher.cs b/src/Compilers/Server/VBCSCompiler/ServerDispatcher.cs index 5ad9aaa1f95e..95e5bf5fbd19 100644 --- a/src/Compilers/Server/VBCSCompiler/ServerDispatcher.cs +++ b/src/Compilers/Server/VBCSCompiler/ServerDispatcher.cs @@ -60,7 +60,7 @@ private enum State private Task? _gcTask; private Task? _listenTask; private readonly List> _connectionList = new List>(); - private TimeSpan? _keepAlive; + private TimeSpan _keepAlive; private bool _keepAliveIsDefault; internal ServerDispatcher(ICompilerServerHost compilerServerHost, IClientConnectionHost clientConnectionHost, IDiagnosticListener? diagnosticListener = null) @@ -78,7 +78,7 @@ internal ServerDispatcher(ICompilerServerHost compilerServerHost, IClientConnect /// accepting new connections and wait for existing connections to complete before /// returning. /// - public void ListenAndDispatchConnections(TimeSpan? keepAlive, CancellationToken cancellationToken = default) + public void ListenAndDispatchConnections(TimeSpan keepAlive, CancellationToken cancellationToken = default) { _state = State.Running; _keepAlive = keepAlive; @@ -230,7 +230,9 @@ private void ChangeToShuttingDown(string reason) private void RunGC() { _gcTask = null; - GC.GetTotalMemory(forceFullCollection: true); + var before = GC.GetTotalMemory(forceFullCollection: false); + var after = GC.GetTotalMemory(forceFullCollection: true); + _logger.Log($"GC completed: before={before} bytes, after={after} bytes"); } private void MaybeCreateListenTask() @@ -244,10 +246,10 @@ private void MaybeCreateListenTask() private void MaybeCreateTimeoutTask() { // If there are no active clients running then the server needs to be in a timeout mode. - if (_state == State.Running && _connectionList.Count == 0 && _timeoutTask is null && _keepAlive.HasValue) + if (_state == State.Running && _connectionList.Count == 0 && _timeoutTask is null && _keepAlive != Timeout.InfiniteTimeSpan) { Debug.Assert(_listenTask != null); - _timeoutTask = Task.Delay(_keepAlive.Value); + _timeoutTask = Task.Delay(_keepAlive); } } @@ -321,7 +323,7 @@ private void HandleCompletedConnections() private void ChangeKeepAlive(TimeSpan keepAlive) { - if (_keepAliveIsDefault || !_keepAlive.HasValue || keepAlive > _keepAlive.Value) + if (_keepAliveIsDefault || _keepAlive == Timeout.InfiniteTimeSpan || keepAlive > _keepAlive) { _keepAlive = keepAlive; _keepAliveIsDefault = false; diff --git a/src/Compilers/Server/VBCSCompiler/VBCSCompiler.cs b/src/Compilers/Server/VBCSCompiler/VBCSCompiler.cs index 7e5c1b39b1a5..cb4d8e26986d 100644 --- a/src/Compilers/Server/VBCSCompiler/VBCSCompiler.cs +++ b/src/Compilers/Server/VBCSCompiler/VBCSCompiler.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Specialized; using System.Diagnostics; using Microsoft.CodeAnalysis.CommandLine; @@ -13,35 +12,23 @@ internal static class VBCSCompiler { public static int Main(string[] args) { - using var logger = new CompilerServerLogger($"VBCSCompiler {Process.GetCurrentProcess().Id}"); - - NameValueCollection appSettings; - try + // Pre-parse arguments to extract the log file path and other values so the logger can be + // initialized with the correct path before the controller is created. + if (!BuildServerController.ParseCommandLine(args, out var pipeName, out var shutdown, out var keepAlive, out var logFilePath)) { -#if BOOTSTRAP - ExitingTraceListener.Install(logger); -#endif + return CommonCompiler.Failed; + } -#if NET472 - appSettings = System.Configuration.ConfigurationManager.AppSettings; -#else - // Do not use AppSettings on non-desktop platforms - appSettings = new NameValueCollection(); + using var logger = new CompilerServerLogger($"VBCSCompiler {Process.GetCurrentProcess().Id}", logFilePath); + +#if BOOTSTRAP + ExitingTraceListener.Install(logger); #endif - } - catch (Exception ex) - { - // It is possible for AppSettings to throw when the application or machine configuration - // is corrupted. This should not prevent the server from starting, but instead just revert - // to the default configuration. - appSettings = new NameValueCollection(); - logger.LogException(ex, "Error loading application settings"); - } try { - var controller = new BuildServerController(appSettings, logger); - return controller.Run(args); + var controller = new BuildServerController(logger); + return controller.Run(pipeName, shutdown, keepAlive); } catch (Exception e) { diff --git a/src/Compilers/Server/VBCSCompilerTests/BuildServerConnectionTests.cs b/src/Compilers/Server/VBCSCompilerTests/BuildServerConnectionTests.cs index 65dc07421233..693ffaff1442 100644 --- a/src/Compilers/Server/VBCSCompilerTests/BuildServerConnectionTests.cs +++ b/src/Compilers/Server/VBCSCompilerTests/BuildServerConnectionTests.cs @@ -145,7 +145,7 @@ public void GetServerEnvironmentVariables_IncludesDotNetRoot() // This test verifies that GetServerEnvironmentVariables properly sets up DOTNET_ROOT // without modifying the current process environment var currentEnvironment = Environment.GetEnvironmentVariables(); - var originalDotNetRoot = currentEnvironment[RuntimeHostInfo.DotNetRootEnvironmentName]; + var originalDotNetRoot = (string?)currentEnvironment[RuntimeHostInfo.DotNetRootEnvironmentName]; var envVars = BuildServerConnection.GetServerEnvironmentVariables(currentEnvironment); @@ -162,10 +162,11 @@ public void GetServerEnvironmentVariables_IncludesDotNetRoot() // Should not have modified the current process environment Assert.Equal(originalDotNetRoot, Environment.GetEnvironmentVariable(RuntimeHostInfo.DotNetRootEnvironmentName)); } - else + else if (envVars != null) { - // If no DOTNET_ROOT is needed, should return null - Assert.Null(envVars); + // No DOTNET_ROOT modification is needed + var modifiedDotNetRoot = envVars.TryGetValue(RuntimeHostInfo.DotNetRootEnvironmentName, out var value) ? value : null; + Assert.Equal(originalDotNetRoot, modifiedDotNetRoot); } } @@ -190,8 +191,10 @@ public void GetServerEnvironmentVariables_ExcludesDotNetRootVariants() var envVars = BuildServerConnection.GetServerEnvironmentVariables(testEnvironment); - if (envVars != null) + if (BuildServerConnection.IsBuiltinToolRunningOnCoreClr && RuntimeHostInfo.GetToolDotNetRoot(Logger.Log) != null) { + Assert.NotNull(envVars); + // Should set DOTNET_ROOT* variants to empty string to prevent inheritance foreach (var testEnvVar in testEnvVars) { diff --git a/src/Compilers/Server/VBCSCompilerTests/BuildServerControllerTests.cs b/src/Compilers/Server/VBCSCompilerTests/BuildServerControllerTests.cs index 56efd7ddaaf9..a4d9dd1432c5 100644 --- a/src/Compilers/Server/VBCSCompilerTests/BuildServerControllerTests.cs +++ b/src/Compilers/Server/VBCSCompilerTests/BuildServerControllerTests.cs @@ -6,8 +6,9 @@ using System; using System.Collections.Specialized; +using System.Threading; +using Microsoft.CodeAnalysis.CommandLine; using Xunit; -using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.CompilerServer.UnitTests { @@ -18,34 +19,28 @@ public void Dispose() NamedPipeTestUtil.DisposeAll(); } - public sealed class GetKeepAliveTimeoutTests + public sealed class GetDefaultKeepAliveTests { private readonly NameValueCollection _appSettings = new NameValueCollection(); - private readonly BuildServerController _controller; - - public GetKeepAliveTimeoutTests(ITestOutputHelper testOutputHelper) - { - _controller = new BuildServerController(_appSettings, new XunitCompilerServerLogger(testOutputHelper)); - } [Fact] public void Simple() { _appSettings[BuildServerController.KeepAliveSettingName] = "42"; - Assert.Equal(TimeSpan.FromSeconds(42), _controller.GetKeepAliveTimeout()); + Assert.Equal(TimeSpan.FromSeconds(42), BuildServerController.GetDefaultKeepAlive(EmptyCompilerServerLogger.Instance, _appSettings)); } [Fact] public void InvalidNumber() { _appSettings[BuildServerController.KeepAliveSettingName] = "dog"; - Assert.Equal(ServerDispatcher.DefaultServerKeepAlive, _controller.GetKeepAliveTimeout()); + Assert.Equal(ServerDispatcher.DefaultServerKeepAlive, BuildServerController.GetDefaultKeepAlive(EmptyCompilerServerLogger.Instance, _appSettings)); } [Fact] public void NoSetting() { - Assert.Equal(ServerDispatcher.DefaultServerKeepAlive, _controller.GetKeepAliveTimeout()); + Assert.Equal(ServerDispatcher.DefaultServerKeepAlive, BuildServerController.GetDefaultKeepAlive(EmptyCompilerServerLogger.Instance, _appSettings)); } } } diff --git a/src/Compilers/Server/VBCSCompilerTests/CompilerServerApiTest.cs b/src/Compilers/Server/VBCSCompilerTests/CompilerServerApiTest.cs index d8f7a830b2a5..5ae914e6c5c3 100644 --- a/src/Compilers/Server/VBCSCompilerTests/CompilerServerApiTest.cs +++ b/src/Compilers/Server/VBCSCompilerTests/CompilerServerApiTest.cs @@ -101,7 +101,7 @@ public void MutexAcquiredWhenRunningServer() var result = BuildServerController.CreateAndRunServer( pipeName, clientConnectionHost: host, - keepAlive: TimeSpan.FromMilliseconds(-1)); + keepAlive: Timeout.InfiniteTimeSpan); Assert.Equal(CommonCompiler.Succeeded, result); Assert.True(wasServerMutexOpen); } diff --git a/src/Compilers/Server/VBCSCompilerTests/CompilerServerTests.cs b/src/Compilers/Server/VBCSCompilerTests/CompilerServerTests.cs index e52547c13a87..152281a713a0 100644 --- a/src/Compilers/Server/VBCSCompilerTests/CompilerServerTests.cs +++ b/src/Compilers/Server/VBCSCompilerTests/CompilerServerTests.cs @@ -1420,24 +1420,6 @@ public async Task Bug1024619_02() Assert.Equal(2, listener.CompletionDataList.Count); } - [WorkItem(406649, "https://devdiv.visualstudio.com/DevDiv/_workitems?id=406649")] - [ConditionalFact(typeof(DesktopOnly))] - public void MissingCompilerAssembly_CompilerServer() - { - var basePath = Path.GetDirectoryName(typeof(CompilerServerUnitTests).Assembly.Location); - var compilerServerExecutable = Path.Combine(basePath, "VBCSCompiler.exe"); - var dir = Temp.CreateDirectory(); - var workingDirectory = dir.Path; - var serverExe = dir.CopyFile(compilerServerExecutable).Path; - dir.CopyFile(typeof(System.Collections.Immutable.ImmutableArray).Assembly.Location); - - // Missing Microsoft.CodeAnalysis.dll launching server. - var result = ProcessUtilities.Run(serverExe, arguments: $"-pipename:{GetUniqueName()}", workingDirectory: workingDirectory); - Assert.Equal(1, result.ExitCode); - // Exception is logged rather than written to output/error streams. - Assert.Equal("", result.Output.Trim()); - } - [WorkItem(406649, "https://devdiv.visualstudio.com/DevDiv/_workitems?id=406649")] [WorkItem(19213, "https://github.com/dotnet/roslyn/issues/19213")] [Fact(Skip = "19213")] diff --git a/src/Compilers/Server/VBCSCompilerTests/ServerUtil.cs b/src/Compilers/Server/VBCSCompilerTests/ServerUtil.cs index 98ae477ad639..854bf8ac4e0a 100644 --- a/src/Compilers/Server/VBCSCompilerTests/ServerUtil.cs +++ b/src/Compilers/Server/VBCSCompilerTests/ServerUtil.cs @@ -85,7 +85,7 @@ internal static ServerData Create( pipeName ??= ServerUtil.GetPipeName(); compilerServerHost ??= BuildServerController.CreateCompilerServerHost(logger); clientConnectionHost ??= BuildServerController.CreateClientConnectionHost(pipeName, logger); - keepAlive ??= TimeSpan.FromMilliseconds(-1); + keepAlive ??= Timeout.InfiniteTimeSpan; var listener = new TestableDiagnosticListener(); var serverListenSource = new TaskCompletionSource(); diff --git a/src/Compilers/Server/VBCSCompilerTests/VBCSCompilerServerTests.cs b/src/Compilers/Server/VBCSCompilerTests/VBCSCompilerServerTests.cs index a71a309f6ea3..367ea43bfc1f 100644 --- a/src/Compilers/Server/VBCSCompilerTests/VBCSCompilerServerTests.cs +++ b/src/Compilers/Server/VBCSCompilerTests/VBCSCompilerServerTests.cs @@ -10,7 +10,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.IO; using System.IO.Pipes; using System.Linq; @@ -56,8 +55,7 @@ public ShutdownTests(ITestOutputHelper testOutputHelper) private Task RunShutdownAsync(string pipeName, bool waitForProcess = true, CancellationToken cancellationToken = default(CancellationToken)) { - var appSettings = new NameValueCollection(); - return new BuildServerController(appSettings, Logger).RunShutdownAsync(pipeName, waitForProcess, Timeout.Infinite, cancellationToken); + return new BuildServerController(Logger).RunShutdownAsync(pipeName, waitForProcess, Timeout.Infinite, cancellationToken); } [Fact] @@ -409,10 +407,12 @@ public class ParseCommandLineTests : VBCSCompilerServerTests { private string _pipeName; private bool _shutdown; + private TimeSpan? _timeout; + private string _logFilePath; private bool Parse(params string[] args) { - return BuildServerController.ParseCommandLine(args, out _pipeName, out _shutdown); + return BuildServerController.ParseCommandLine(args, out _pipeName, out _shutdown, out _timeout, out _logFilePath); } [Fact] @@ -421,6 +421,8 @@ public void Nothing() Assert.True(Parse()); Assert.Null(_pipeName); Assert.False(_shutdown); + Assert.Null(_timeout); + Assert.Null(_logFilePath); } [Fact] @@ -453,6 +455,52 @@ public void BadArg() Assert.False(Parse("-invalid")); Assert.False(Parse("name")); } + + [Fact] + public void TimeoutSeconds() + { + Assert.True(Parse("-timeout:60")); + Assert.Equal(TimeSpan.FromSeconds(60), _timeout); + } + + [Fact] + public void TimeoutNoTimeout() + { + Assert.True(Parse("-timeout:0")); + Assert.Equal(Timeout.InfiniteTimeSpan, _timeout); + } + + [Fact] + public void TimeoutInvalid() + { + Assert.False(Parse("-timeout:abc")); + Assert.False(Parse("-timeout:-1")); + Assert.False(Parse("-timeout:-2")); + Assert.False(Parse("-timeout:")); + } + + [Fact] + public void LogFilePathEmpty() + { + Assert.False(Parse("-log:")); + } + + [Fact] + public void LogFilePath() + { + Assert.True(Parse("-log:/tmp/server.log")); + Assert.Equal("/tmp/server.log", _logFilePath); + } + + [Fact] + public void AllArgs() + { + Assert.True(Parse("-pipename:test", "-timeout:120", "-log:/tmp/server.log")); + Assert.Equal("test", _pipeName); + Assert.Equal(TimeSpan.FromSeconds(120), _timeout); + Assert.Equal("/tmp/server.log", _logFilePath); + Assert.False(_shutdown); + } } } } diff --git a/src/Compilers/Shared/BuildServerConnection.cs b/src/Compilers/Shared/BuildServerConnection.cs index a5e940281e7c..ccc84a4b749b 100644 --- a/src/Compilers/Shared/BuildServerConnection.cs +++ b/src/Compilers/Shared/BuildServerConnection.cs @@ -412,7 +412,7 @@ internal static async Task MonitorDisconnectAsync( // IOException: The server is connected to another client and the // time-out period has expired. - logger.LogException(e, $"Connecting to server timed out after {timeoutMs} ms"); + logger.Log($"Connecting to server timed out after {timeoutMs} ms: '{e.GetType().Name}' '{e.Message}'"); pipeStream.Dispose(); return null; } @@ -424,7 +424,7 @@ internal static async Task MonitorDisconnectAsync( if (!NamedPipeUtil.CheckPipeConnectionOwnership(pipeStream)) { pipeStream.Dispose(); - logger.LogError("Owner of named pipe is incorrect"); + logger.Log("Owner of named pipe is incorrect"); return null; } @@ -432,7 +432,7 @@ internal static async Task MonitorDisconnectAsync( } catch (Exception e) when (!(e is TaskCanceledException || e is OperationCanceledException)) { - logger.LogException(e, "Exception while connecting to process"); + logger.Log($"Exception while connecting to process: '{e.GetType().Name}' '{e.Message}'"); pipeStream?.Dispose(); return null; } @@ -490,7 +490,9 @@ private static IntPtr CreateEnvironmentBlock(Dictionary environm /// Dictionary of environment variables to set, or null if no custom environment is needed internal static Dictionary? GetServerEnvironmentVariables(System.Collections.IDictionary currentEnvironment, ICompilerServerLogger? logger = null) { - if (!IsBuiltinToolRunningOnCoreClr || RuntimeHostInfo.GetToolDotNetRoot(logger is null ? null : logger.Log) is not { } dotNetRoot) + string? dotNetRoot = IsBuiltinToolRunningOnCoreClr ? RuntimeHostInfo.GetToolDotNetRoot(logger is null ? null : logger.Log) : null; + + if (dotNetRoot == null && !RuntimeHostInfo.ShouldDisableTieredCompilation) { return null; } @@ -504,7 +506,7 @@ private static IntPtr CreateEnvironmentBlock(Dictionary environm // Clear DOTNET_ROOT* variables such as DOTNET_ROOT_X64 by setting them to empty, // as we want to set our own DOTNET_ROOT and avoid conflicts - if (key.StartsWith(RuntimeHostInfo.DotNetRootEnvironmentName, StringComparison.OrdinalIgnoreCase)) + if (dotNetRoot != null && key.StartsWith(RuntimeHostInfo.DotNetRootEnvironmentName, StringComparison.OrdinalIgnoreCase)) { environmentVariables[key] = string.Empty; } @@ -515,9 +517,18 @@ private static IntPtr CreateEnvironmentBlock(Dictionary environm } // Set our DOTNET_ROOT - environmentVariables[RuntimeHostInfo.DotNetRootEnvironmentName] = dotNetRoot; + if (dotNetRoot != null) + { + logger?.Log("Setting {0} to '{1}'", RuntimeHostInfo.DotNetRootEnvironmentName, dotNetRoot); + environmentVariables[RuntimeHostInfo.DotNetRootEnvironmentName] = dotNetRoot; + } - logger?.Log("Setting {0} to '{1}'", RuntimeHostInfo.DotNetRootEnvironmentName, dotNetRoot); + if (RuntimeHostInfo.ShouldDisableTieredCompilation && !environmentVariables.ContainsKey(RuntimeHostInfo.DotNetTieredCompilationEnvironmentName)) + { + var value = "0"; + logger?.Log("Setting {0} to '{1}'", RuntimeHostInfo.DotNetTieredCompilationEnvironmentName, value); + environmentVariables[RuntimeHostInfo.DotNetTieredCompilationEnvironmentName] = value; + } return environmentVariables; } diff --git a/src/Compilers/Shared/RuntimeHostInfo.cs b/src/Compilers/Shared/RuntimeHostInfo.cs index e020b3a165ae..277e65d2773e 100644 --- a/src/Compilers/Shared/RuntimeHostInfo.cs +++ b/src/Compilers/Shared/RuntimeHostInfo.cs @@ -26,9 +26,16 @@ internal static class RuntimeHostInfo false; #endif + /// + /// Disable JIT tiered compilation on .NET Framework (i.e., keep it enabled on 'dotnet build' but not 'msbuild' which would slow down VS startup perf). + /// The caller should also check that the environment variable is not already set to avoid overriding user preferences. + /// + internal static bool ShouldDisableTieredCompilation => !IsCoreClrRuntime; + internal const string DotNetRootEnvironmentName = "DOTNET_ROOT"; internal const string DotNetHostPathEnvironmentName = "DOTNET_HOST_PATH"; internal const string DotNetExperimentalHostPathEnvironmentName = "DOTNET_EXPERIMENTAL_HOST_PATH"; + internal const string DotNetTieredCompilationEnvironmentName = "DOTNET_TieredCompilation"; /// /// The DOTNET_ROOT that should be used when launching executable tools. diff --git a/src/Compilers/Test/Core/Compilation/OperationTreeVerifier.cs b/src/Compilers/Test/Core/Compilation/OperationTreeVerifier.cs index 719323071b96..86a93337f763 100644 --- a/src/Compilers/Test/Core/Compilation/OperationTreeVerifier.cs +++ b/src/Compilers/Test/Core/Compilation/OperationTreeVerifier.cs @@ -242,7 +242,13 @@ private void LogConversion(CommonConversion conversion, string header = "Convers var isReference = FormatBoolProperty(nameof(conversion.IsReference), conversion.IsReference); var isUserDefined = FormatBoolProperty(nameof(conversion.IsUserDefined), conversion.IsUserDefined); - LogString($"{header}: {nameof(CommonConversion)} ({exists}, {isIdentity}, {isNumeric}, {isReference}, {isUserDefined}) ("); + string isUnion = ""; + if (conversion.IsUnion) + { + isUnion = ", " + FormatBoolProperty(nameof(conversion.IsUnion), conversion.IsUnion); + } + + LogString($"{header}: {nameof(CommonConversion)} ({exists}, {isIdentity}, {isNumeric}, {isReference}, {isUserDefined}{isUnion}) ("); LogSymbol(conversion.MethodSymbol, nameof(conversion.MethodSymbol)); LogString(")"); } diff --git a/src/Compilers/Test/Core/Diagnostics/TrackingDiagnosticAnalyzer.cs b/src/Compilers/Test/Core/Diagnostics/TrackingDiagnosticAnalyzer.cs index aa53734438ba..593f84319806 100644 --- a/src/Compilers/Test/Core/Diagnostics/TrackingDiagnosticAnalyzer.cs +++ b/src/Compilers/Test/Core/Diagnostics/TrackingDiagnosticAnalyzer.cs @@ -64,7 +64,7 @@ public IEnumerable CallLog #region Analysis private static readonly Regex s_omittedSyntaxKindRegex = - new Regex(@"None|Trivia|Token|Keyword|List|Xml|Cref|Compilation|Namespace|Class|Struct|Enum|Interface|Delegate|Field|Property|Indexer|Event|Operator|Constructor|Access|Incomplete|Attribute|Filter|InterpolatedString"); + new Regex(@"None|Trivia|Token|Keyword|List|Xml|Cref|Compilation|Namespace|Class|Struct|Union|Enum|Interface|Delegate|Field|Property|Indexer|Event|Operator|Constructor|Access|Incomplete|Attribute|Filter|InterpolatedString"); private bool FilterByAbstractName(Entry entry, string abstractMemberName) { diff --git a/src/Compilers/Test/Core/Metadata/MetadataReaderUtils.cs b/src/Compilers/Test/Core/Metadata/MetadataReaderUtils.cs index 1486d5d4f9f3..5443a530073b 100644 --- a/src/Compilers/Test/Core/Metadata/MetadataReaderUtils.cs +++ b/src/Compilers/Test/Core/Metadata/MetadataReaderUtils.cs @@ -19,6 +19,7 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; +using System.Buffers.Binary; namespace Roslyn.Test.Utilities { @@ -281,7 +282,7 @@ where reader.GetGuid(cdi.Kind) == PortableCustomDebugInfoKinds.EmbeddedSource return null; } - int uncompressedSize = BitConverter.ToInt32(bytes, 0); + int uncompressedSize = BinaryPrimitives.ReadInt32LittleEndian(bytes.AsSpan()); var stream = new MemoryStream(bytes, sizeof(int), bytes.Length - sizeof(int)); if (uncompressedSize != 0) diff --git a/src/Compilers/Test/Core/TempFiles/TempRoot.cs b/src/Compilers/Test/Core/TempFiles/TempRoot.cs index 013d674150ad..d1c95b7c5681 100644 --- a/src/Compilers/Test/Core/TempFiles/TempRoot.cs +++ b/src/Compilers/Test/Core/TempFiles/TempRoot.cs @@ -19,7 +19,35 @@ public sealed class TempRoot : IDisposable static TempRoot() { - Root = Path.Combine(Path.GetTempPath(), "RoslynTests"); + var tempDirectory = new DirectoryInfo(Path.GetTempPath()); + +#if NET + // When running on MacOS, `Path.GetTempPath()` will return "/var/folders/..." which is a symlink to "/private/var/folders/...". This + // can cause issues when watching files under the temp directory, as the FileSystemWatcher will report changes using the real path. + // So, we need to adjust this path by walking up the temp path until we find a directory that is a link and resolve it. + + if (tempDirectory.LinkTarget != null) + { + tempDirectory = (DirectoryInfo)Directory.ResolveLinkTarget(tempDirectory.FullName, returnFinalTarget: true); + } + else + { + var parentDirectory = tempDirectory.Parent; + while (parentDirectory != null && parentDirectory.LinkTarget == null) + { + parentDirectory = parentDirectory.Parent; + } + + if (parentDirectory != null) + { + var relativePath = Path.GetRelativePath(parentDirectory.FullName, tempDirectory.FullName); + var realPath = Directory.ResolveLinkTarget(parentDirectory.FullName, returnFinalTarget: true).FullName; + tempDirectory = new DirectoryInfo(Path.GetFullPath(Path.Combine(realPath, relativePath))); + } + } +#endif + + Root = Path.Combine(tempDirectory.FullName, "RoslynTests"); Directory.CreateDirectory(Root); } diff --git a/src/Compilers/Test/Core/TestHelpers.cs b/src/Compilers/Test/Core/TestHelpers.cs index e221647a181f..bb7e64edbfe5 100644 --- a/src/Compilers/Test/Core/TestHelpers.cs +++ b/src/Compilers/Test/Core/TestHelpers.cs @@ -9,6 +9,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; @@ -176,5 +177,12 @@ byte parseByte(ReadOnlySpan input, NumberStyles numberStyle) #endif } } + + /// + /// Create an absolute path for the current OS platform with the given suffix. + /// NOTE: the path is not appropriate for actually writing files during tests, use TempRoot instead for that. + /// + public static string CreateAbsolutePath(string suffix) + => Path.Combine(Path.GetTempPath(), suffix); } } diff --git a/src/Compilers/Test/Core/TestResource.resx b/src/Compilers/Test/Core/TestResource.resx index 53be81b8581f..39b69cb8ff18 100644 --- a/src/Compilers/Test/Core/TestResource.resx +++ b/src/Compilers/Test/Core/TestResource.resx @@ -898,6 +898,10 @@ class CollectionExpressions static int[] X = [with()]; } +union U1(int, string) +{ +} + #line 6 #line 2 "test.cs" #line default diff --git a/src/Compilers/Test/Core/Traits/CompilerFeature.cs b/src/Compilers/Test/Core/Traits/CompilerFeature.cs index 033dd1a6eee4..0e98ba20d05c 100644 --- a/src/Compilers/Test/Core/Traits/CompilerFeature.cs +++ b/src/Compilers/Test/Core/Traits/CompilerFeature.cs @@ -44,6 +44,7 @@ public enum CompilerFeature RequiredMembers, RefLifetime, Extensions, + Unsafe, CollectionExpressions, } } diff --git a/src/Compilers/Test/Resources/Core/MetadataTests/Invalid/Obfuscated.dll b/src/Compilers/Test/Resources/Core/MetadataTests/Invalid/Obfuscated.dll deleted file mode 100644 index 80fda33fa5bf..000000000000 Binary files a/src/Compilers/Test/Resources/Core/MetadataTests/Invalid/Obfuscated.dll and /dev/null differ diff --git a/src/Compilers/Test/Resources/Core/MetadataTests/Invalid/Obfuscated2.dll b/src/Compilers/Test/Resources/Core/MetadataTests/Invalid/Obfuscated2.dll deleted file mode 100644 index 70a126e5dcba..000000000000 Binary files a/src/Compilers/Test/Resources/Core/MetadataTests/Invalid/Obfuscated2.dll and /dev/null differ diff --git a/src/Compilers/Test/Resources/Core/Microsoft.CodeAnalysis.Compiler.Test.Resources.csproj b/src/Compilers/Test/Resources/Core/Microsoft.CodeAnalysis.Compiler.Test.Resources.csproj index 2e52cad71d47..ff3547fe2d86 100644 --- a/src/Compilers/Test/Resources/Core/Microsoft.CodeAnalysis.Compiler.Test.Resources.csproj +++ b/src/Compilers/Test/Resources/Core/Microsoft.CodeAnalysis.Compiler.Test.Resources.csproj @@ -12,6 +12,7 @@ This is an internal package for testing. Not meant for external or production uses. The API can and will break at our discretion. $(NoWarn),NU5100,NU5106,CS1591 true + $(DefineConstants);DOTNET_BUILD_FROM_VMR @@ -60,8 +61,6 @@ - - diff --git a/src/Compilers/Test/Resources/Core/TestResources.cs b/src/Compilers/Test/Resources/Core/TestResources.cs index a7fed061b198..14998bde934f 100644 --- a/src/Compilers/Test/Resources/Core/TestResources.cs +++ b/src/Compilers/Test/Resources/Core/TestResources.cs @@ -127,12 +127,6 @@ public static class Invalid private static string s_manyMethodSpecs; public static string ManyMethodSpecs => ResourceLoader.GetOrCreateResource(ref s_manyMethodSpecs, "MetadataTests.Invalid.ManyMethodSpecs.vb"); - private static byte[] s_obfuscated; - public static byte[] Obfuscated => ResourceLoader.GetOrCreateResource(ref s_obfuscated, "MetadataTests.Invalid.Obfuscated.dll"); - - private static byte[] s_obfuscated2; - public static byte[] Obfuscated2 => ResourceLoader.GetOrCreateResource(ref s_obfuscated2, "MetadataTests.Invalid.Obfuscated2.dll"); - public static class Signatures { private static byte[] s_signatureCycle2; diff --git a/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs b/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs index 56236ce6883c..701429b56262 100644 --- a/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs +++ b/src/Compilers/Test/Utilities/CSharp/CSharpTestBase.cs @@ -38,6 +38,27 @@ public abstract class CSharpTestBase : CommonTestBase { public static readonly TheoryData LanguageVersions13AndNewer = new TheoryData([LanguageVersion.CSharp13, LanguageVersion.Preview, LanguageVersion.CSharp14]); + protected static readonly string IUnionSource = @" +namespace System.Runtime.CompilerServices +{ + public interface IUnion + { +#nullable enable +#line 100000 + object? Value { get; } +#nullable disable + } +} +"; + protected static readonly string UnionAttributeSource = @" +namespace System.Runtime.CompilerServices +{ + public class UnionAttribute : System.Attribute + { + } +} +"; + protected static readonly string NullableAttributeDefinition = @" namespace System.Runtime.CompilerServices { @@ -687,6 +708,26 @@ public sealed class RefSafetyRulesAttribute : Attribute protected static MetadataReference RefSafetyRulesAttributeLib => CreateCompilation(RefSafetyRulesAttributeDefinition).EmitToImageReference(); + protected static readonly string MemorySafetyRulesAttributeDefinition = """ + namespace System.Runtime.CompilerServices + { + [AttributeUsage(AttributeTargets.Module, Inherited = false, AllowMultiple = false)] + public sealed class MemorySafetyRulesAttribute : Attribute + { + public MemorySafetyRulesAttribute(int version) { Version = version; } + public int Version { get; } + } + } + """; + + protected static readonly string RequiresUnsafeAttributeDefinition = """ + namespace System.Diagnostics.CodeAnalysis + { + [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Event | AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] + public sealed class RequiresUnsafeAttribute : Attribute { } + } + """; + protected static readonly string RequiredMemberAttribute = @" namespace System.Runtime.CompilerServices { @@ -1299,8 +1340,11 @@ public static void UnsafeAwaitAwaiter(TAwaiter awaiter) where TAwaiter internal const string RuntimeAsyncMethodGenerationAttributeDefinition = """ namespace System.Runtime.CompilerServices; + #pragma warning disable CS9113 // Unread primary constructor parameter + [AttributeUsage(AttributeTargets.Method)] public class RuntimeAsyncMethodGenerationAttribute(bool runtimeAsync) : Attribute(); + #pragma warning restore CS9113 // Unread primary constructor parameter """; protected static T GetSyntax(SyntaxTree tree, string text, bool descendIntoTrivia = false) @@ -3255,5 +3299,46 @@ public interface IAsyncDisposable } } "; + + protected static void VerifyDecisionDagDump(Compilation comp, string expectedDecisionDag, int index = 0, bool forLowering = false) + where T : CSharpSyntaxNode + { +#if DEBUG + var tree = comp.SyntaxTrees.First(); + var node = tree.GetRoot().DescendantNodes().OfType().ElementAt(index); + var model = (CSharpSemanticModel)comp.GetSemanticModel(tree); + var binder = model.GetEnclosingBinder(node.SpanStart); + BoundDecisionDag decisionDag; + + switch (node) + { + case SwitchStatementSyntax n: + { + var b = (BoundSwitchStatement)binder.BindStatement(n, BindingDiagnosticBag.Discarded); + decisionDag = forLowering ? b.GetDecisionDagForLowering((CSharpCompilation)comp) : b.ReachabilityDecisionDag; + } + break; + + case SwitchExpressionSyntax n: + { + var b = (BoundSwitchExpression)binder.BindExpression(n, BindingDiagnosticBag.Discarded); + decisionDag = forLowering ? b.GetDecisionDagForLowering((CSharpCompilation)comp, out _) : b.ReachabilityDecisionDag; + } + break; + + case IsPatternExpressionSyntax n: + { + var b = (BoundIsPatternExpression)binder.BindExpression(n, BindingDiagnosticBag.Discarded); + decisionDag = forLowering ? b.GetDecisionDagForLowering((CSharpCompilation)comp) : b.ReachabilityDecisionDag; + } + break; + + default: + throw ExceptionUtilities.UnexpectedValue(node); + } + + AssertEx.Equal(expectedDecisionDag, decisionDag.Dump()); +#endif + } } } diff --git a/src/Compilers/Test/Utilities/CSharp/Extensions.cs b/src/Compilers/Test/Utilities/CSharp/Extensions.cs index 6b4e838a7672..5743683575a2 100644 --- a/src/Compilers/Test/Utilities/CSharp/Extensions.cs +++ b/src/Compilers/Test/Utilities/CSharp/Extensions.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; @@ -16,7 +15,6 @@ using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; using Xunit; @@ -231,6 +229,12 @@ public static ImmutableArray GetMembers(this Compilation compilation, st return members; } + public static ImmutableArray GetMembersByQualifiedName(this NamespaceOrTypeSymbol container, string qualifiedName) where T : Symbol + => GetMembersByQualifiedName(container, qualifiedName).SelectAsArray(s => (T)s); + + public static ImmutableArray GetMembersByQualifiedName(this NamespaceOrTypeSymbol container, string qualifiedName) + => GetMembers(container, qualifiedName, lastContainer: out _); + private static ImmutableArray GetMembers(NamespaceOrTypeSymbol container, string qualifiedName, out NamespaceOrTypeSymbol lastContainer) { var parts = SplitMemberName(qualifiedName); diff --git a/src/Compilers/Test/Utilities/CSharp/TestOptions.cs b/src/Compilers/Test/Utilities/CSharp/TestOptions.cs index 361a23e4e882..cd947eca67ae 100644 --- a/src/Compilers/Test/Utilities/CSharp/TestOptions.cs +++ b/src/Compilers/Test/Utilities/CSharp/TestOptions.cs @@ -176,6 +176,11 @@ public static CSharpCompilationOptions WithSpecificDiagnosticOptions(this CSharp return options.WithSpecificDiagnosticOptions(ImmutableDictionary.Empty.Add(key1, value).Add(key2, value)); } + public static CSharpCompilationOptions AddSpecificDiagnosticOptions(this CSharpCompilationOptions options, string key, ReportDiagnostic value) + { + return options.WithSpecificDiagnosticOptions(options.SpecificDiagnosticOptions.Add(key, value)); + } + /// /// Create with the maximum warning level. /// diff --git a/src/Compilers/Test/Utilities/CSharp/TestSources.cs b/src/Compilers/Test/Utilities/CSharp/TestSources.cs index 32bd3d098e11..77e1d9504bdf 100644 --- a/src/Compilers/Test/Utilities/CSharp/TestSources.cs +++ b/src/Compilers/Test/Utilities/CSharp/TestSources.cs @@ -99,7 +99,7 @@ public ref T Current public Span Slice(int offset, int length) => new Span(this.arr, offset, length); - public Span Slice(int offset) => new Span(this.arr, offset, Length - offset); + public Span Slice(int offset) => Slice(offset, Length - offset); } public readonly ref struct ReadOnlySpan @@ -191,7 +191,7 @@ public ref readonly T Current public ReadOnlySpan Slice(int offset, int length) => new ReadOnlySpan(this.arr, offset, length); - public ReadOnlySpan Slice(int offset) => new ReadOnlySpan(this.arr, offset, offset - Length); + public ReadOnlySpan Slice(int offset) => Slice(offset, Length - offset); #nullable enable public static ReadOnlySpan CastUp(ReadOnlySpan items) where TDerived : class?, T @@ -574,5 +574,16 @@ public ParamCollectionAttribute() { } } } """; + + public static readonly string InterceptsLocationAttribute = """ + namespace System.Runtime.CompilerServices; + + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public sealed class InterceptsLocationAttribute : Attribute + { + public InterceptsLocationAttribute(string filePath, int line, int character) { } + public InterceptsLocationAttribute(int version, string data) { } + } + """; } } diff --git a/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCommandLineParser.vb b/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCommandLineParser.vb index a53023f771cd..0528e70f4dae 100644 --- a/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCommandLineParser.vb +++ b/src/Compilers/VisualBasic/Portable/CommandLine/VisualBasicCommandLineParser.vb @@ -355,8 +355,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Continue For End If - Dim newChecksumAlgorithm = TryParseHashAlgorithmName(value) - If newChecksumAlgorithm = SourceHashAlgorithm.None Then + Dim newChecksumAlgorithm As SourceHashAlgorithm + If Not SourceHashAlgorithms.TryParseAlgorithmName(value, newChecksumAlgorithm) Then AddDiagnostic(diagnostics, ERRID.ERR_BadChecksumAlgorithm, value) Continue For End If diff --git a/src/Compilers/VisualBasic/Portable/Syntax/MethodBaseSyntax.vb b/src/Compilers/VisualBasic/Portable/Syntax/MethodBaseSyntax.vb index 87c7aebca8eb..37a172b92c87 100644 --- a/src/Compilers/VisualBasic/Portable/Syntax/MethodBaseSyntax.vb +++ b/src/Compilers/VisualBasic/Portable/Syntax/MethodBaseSyntax.vb @@ -48,7 +48,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -56,7 +56,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As MethodStatementSyntax Return WithSubOrFunctionKeyword(keyword) End Function @@ -76,7 +76,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -84,7 +84,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As DelegateStatementSyntax Return WithSubOrFunctionKeyword(keyword) End Function @@ -103,7 +103,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -111,7 +111,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As DeclareStatementSyntax Return WithSubOrFunctionKeyword(keyword) End Function @@ -131,7 +131,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -139,7 +139,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As LambdaHeaderSyntax Return WithSubOrFunctionKeyword(keyword) End Function @@ -159,7 +159,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -167,7 +167,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As SubNewStatementSyntax Return WithSubKeyword(keyword) End Function @@ -187,7 +187,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -195,7 +195,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As EventStatementSyntax Return WithEventKeyword(keyword) End Function @@ -215,7 +215,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -223,7 +223,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As PropertyStatementSyntax Return WithPropertyKeyword(keyword) End Function @@ -243,7 +243,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -251,7 +251,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As OperatorStatementSyntax Return WithOperatorKeyword(keyword) End Function @@ -271,7 +271,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -279,7 +279,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As AccessorStatementSyntax Return WithAccessorKeyword(keyword) End Function diff --git a/src/Compilers/VisualBasic/Portable/Syntax/MethodBlockBaseSyntax.vb b/src/Compilers/VisualBasic/Portable/Syntax/MethodBlockBaseSyntax.vb index 9cccc15fe455..4a3d1edae7e6 100644 --- a/src/Compilers/VisualBasic/Portable/Syntax/MethodBlockBaseSyntax.vb +++ b/src/Compilers/VisualBasic/Portable/Syntax/MethodBlockBaseSyntax.vb @@ -138,7 +138,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Begin As SubNewStatementSyntax Get Return SubNewStatement @@ -146,7 +146,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows ReadOnly Property [End] As EndBlockStatementSyntax Get Return EndSubStatement @@ -154,13 +154,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithBegin(begin As SubNewStatementSyntax) As ConstructorBlockSyntax Return WithSubNewStatement(begin) End Function - + Public Shadows Function WithEnd([end] As EndBlockStatementSyntax) As ConstructorBlockSyntax Return WithEndSubStatement([end]) End Function @@ -190,7 +190,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Begin As MethodStatementSyntax Get Return SubOrFunctionStatement @@ -198,7 +198,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows ReadOnly Property [End] As EndBlockStatementSyntax Get Return EndSubOrFunctionStatement @@ -206,13 +206,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithBegin(begin As MethodStatementSyntax) As MethodBlockSyntax Return WithSubOrFunctionStatement(begin) End Function - + Public Shadows Function WithEnd([end] As EndBlockStatementSyntax) As MethodBlockSyntax Return WithEndSubOrFunctionStatement([end]) End Function @@ -242,7 +242,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Begin As OperatorStatementSyntax Get Return OperatorStatement @@ -250,7 +250,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows ReadOnly Property [End] As EndBlockStatementSyntax Get Return EndOperatorStatement @@ -258,13 +258,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithBegin(begin As OperatorStatementSyntax) As OperatorBlockSyntax Return WithOperatorStatement(begin) End Function - + Public Shadows Function WithEnd([end] As EndBlockStatementSyntax) As OperatorBlockSyntax Return WithEndOperatorStatement([end]) End Function diff --git a/src/Compilers/VisualBasic/Portable/Syntax/SyntaxNodeRemover.vb b/src/Compilers/VisualBasic/Portable/Syntax/SyntaxNodeRemover.vb index c22eeccc6a74..7f112c1f79e9 100644 --- a/src/Compilers/VisualBasic/Portable/Syntax/SyntaxNodeRemover.vb +++ b/src/Compilers/VisualBasic/Portable/Syntax/SyntaxNodeRemover.vb @@ -298,7 +298,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax Me._directivesToKeep.Clear() End If - Dim directivesInSpan = node.DescendantTrivia(span, Function(n) n.ContainsDirectives, descendIntoTrivia:=True) _ + Dim directivesInSpan = node.DescendantTrivia(span, descendIntoChildrenGreen:=Function(n) n.ContainsDirectives, descendIntoChildrenRed:=Nothing, descendIntoTrivia:=True) _ .Where(Function(tr) tr.IsDirective) _ .Select(Function(tr) DirectCast(tr.GetStructure(), DirectiveTriviaSyntax)) diff --git a/src/Compilers/VisualBasic/Portable/Syntax/TypeBlockSyntax.vb b/src/Compilers/VisualBasic/Portable/Syntax/TypeBlockSyntax.vb index c7cefff1c72a..6f0464d359fa 100644 --- a/src/Compilers/VisualBasic/Portable/Syntax/TypeBlockSyntax.vb +++ b/src/Compilers/VisualBasic/Portable/Syntax/TypeBlockSyntax.vb @@ -86,7 +86,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Begin As ClassStatementSyntax Get Return ClassStatement @@ -94,7 +94,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows ReadOnly Property [End] As EndBlockStatementSyntax Get Return EndClassStatement @@ -102,13 +102,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithBegin(begin As ClassStatementSyntax) As ClassBlockSyntax Return WithClassStatement(begin) End Function - + Public Shadows Function WithEnd([end] As EndBlockStatementSyntax) As ClassBlockSyntax Return WithEndClassStatement([end]) End Function @@ -138,7 +138,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Begin As StructureStatementSyntax Get Return StructureStatement @@ -146,7 +146,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows ReadOnly Property [End] As EndBlockStatementSyntax Get Return EndStructureStatement @@ -154,13 +154,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithBegin(begin As StructureStatementSyntax) As StructureBlockSyntax Return WithStructureStatement(begin) End Function - + Public Shadows Function WithEnd([end] As EndBlockStatementSyntax) As StructureBlockSyntax Return WithEndStructureStatement([end]) End Function @@ -190,7 +190,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Begin As InterfaceStatementSyntax Get Return InterfaceStatement @@ -198,7 +198,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows ReadOnly Property [End] As EndBlockStatementSyntax Get Return EndInterfaceStatement @@ -206,13 +206,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithBegin(begin As InterfaceStatementSyntax) As InterfaceBlockSyntax Return WithInterfaceStatement(begin) End Function - + Public Shadows Function WithEnd([end] As EndBlockStatementSyntax) As InterfaceBlockSyntax Return WithEndInterfaceStatement([end]) End Function @@ -242,7 +242,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Begin As ModuleStatementSyntax Get Return ModuleStatement @@ -250,7 +250,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows ReadOnly Property [End] As EndBlockStatementSyntax Get Return EndModuleStatement @@ -258,13 +258,13 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithBegin(begin As ModuleStatementSyntax) As ModuleBlockSyntax Return WithModuleStatement(begin) End Function - + Public Shadows Function WithEnd([end] As EndBlockStatementSyntax) As ModuleBlockSyntax Return WithEndModuleStatement([end]) End Function diff --git a/src/Compilers/VisualBasic/Portable/Syntax/TypeStatementSyntax.vb b/src/Compilers/VisualBasic/Portable/Syntax/TypeStatementSyntax.vb index 043644233c62..c3995b32ef36 100644 --- a/src/Compilers/VisualBasic/Portable/Syntax/TypeStatementSyntax.vb +++ b/src/Compilers/VisualBasic/Portable/Syntax/TypeStatementSyntax.vb @@ -54,7 +54,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -62,7 +62,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As ModuleStatementSyntax Return WithModuleKeyword(keyword) End Function @@ -82,7 +82,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -90,7 +90,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As StructureStatementSyntax Return WithStructureKeyword(keyword) End Function @@ -110,7 +110,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -118,7 +118,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As ClassStatementSyntax Return WithClassKeyword(keyword) End Function @@ -138,7 +138,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Function - + Public Shadows ReadOnly Property Keyword As SyntaxToken Get Return DeclarationKeyword @@ -146,7 +146,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Syntax End Property - + Public Shadows Function WithKeyword(keyword As SyntaxToken) As InterfaceStatementSyntax Return WithInterfaceKeyword(keyword) End Function diff --git a/src/Compilers/VisualBasic/Portable/VBResources.resx b/src/Compilers/VisualBasic/Portable/VBResources.resx index 259c489c8b9b..b51e0b60876c 100644 --- a/src/Compilers/VisualBasic/Portable/VBResources.resx +++ b/src/Compilers/VisualBasic/Portable/VBResources.resx @@ -5164,7 +5164,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.cs.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.cs.xlf index e13d4dfc3357..4665e38e3c2b 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.cs.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.cs.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -391,173 +391,173 @@ -vbruntime:<file> Compile with the alternate Visual Basic runtime in <file>. - Visual Basic Compiler Options + Možnosti kompilátoru Visual Basic - - OUTPUT FILE - --out:<file> Specifies the output file name. --target:exe Create a console application (default). - (Short form: -t) --target:winexe Create a Windows application. --target:library Create a library assembly. --target:module Create a module that can be added to an - assembly. --target:appcontainerexe Create a Windows application that runs in + - VÝSTUPNÍ SOUBOR - +-out:<file> Určuje název výstupního souboru. +-target:exe Vytvoří aplikaci konzole (výchozí). + (Krátký tvar: -t) +-target:winexe Vytvoří aplikaci systému Windows. +-target:library Vytvoří sestavení knihovny. +-target:module Vytvoří modul, který se dá přidat k + sestavení. +-target:appcontainerexe Vytvoří aplikaci systému Windows, která běží v AppContainer. --target:winmdobj Create a Windows Metadata intermediate file --doc[+|-] Generates XML documentation file. --doc:<file> Generates XML documentation file to <file>. --refout:<file> Reference assembly output to generate +-target:winmdobj Vytvoří pomocný soubor metadat systému Windows +-doc[+|-] Vygeneruje soubor dokumentace XML. +-doc:<file> Vygeneruje soubor dokumentace XML do souboru <file>. +-refout:<file> Výstup referenčního sestavení, který se má vygenerovat - - INPUT FILES - --addmodule:<file_list> Reference metadata from the specified modules --link:<file_list> Embed metadata from the specified interop - assembly. (Short form: -l) --recurse:<wildcard> Include all files in the current directory - and subdirectories according to the - wildcard specifications. --reference:<file_list> Reference metadata from the specified - assembly. (Short form: -r) --analyzer:<file_list> Run the analyzers from this assembly - (Short form: -a) --additionalfile:<file list> Additional files that don't directly affect code - generation but may be used by analyzers for producing - errors or warnings. + - VSTUPNÍ SOUBORY - +-addmodule:<file_list> Odkazuje na metadata ze zadaných modulů +-link:<file_list> Vloží metadata ze zadaného definičního + sestavení. (Krátký tvar: -l) +-recurse:<wildcard> Zahrne všechny soubory v aktuálním adresáři + a podadresářích podle + specifikace zástupných znaků. +-reference:<file_list> Odkazuje na metadata ze zadaného + sestavení. (Krátký tvar: -r) +-analyzer:<file_list> Spustí analyzátory z tohoto sestavení + (Krátký tvar: -a) +-additionalfile:<file list> Další soubory, které nemají přímý vliv na generování + kódu, ale můžou je používat analyzátory k vytváření + chyb nebo upozornění. - - RESOURCES - --linkresource:<resinfo> Links the specified file as an external - assembly resource. + - PROSTŘEDKY - +-linkresource:<resinfo> Propojuje zadaný soubor jako externí + prostředek sestavení. resinfo:<file>[,<name>[,public|private]] - (Short form: -linkres) --nowin32manifest The default manifest should not be embedded - in the manifest section of the output PE. --resource:<resinfo> Adds the specified file as an embedded - assembly resource. + (Krátký tvar: -linkres) +-nowin32manifest Výchozí manifest by neměl být vložený + v oddílu manifestu výstupních dat PE. +-resource:<resinfo> Přidá zadaný soubor jako vložený + prostředek sestavení. resinfo:<file>[,<name>[,public|private]] - (Short form: -res) --win32icon:<file> Specifies a Win32 icon file (.ico) for the - default Win32 resources. --win32manifest:<file> The provided file is embedded in the manifest - section of the output PE. --win32resource:<file> Specifies a Win32 resource file (.res). + (Krátký tvar: -res) +-win32icon:<file> Určuje soubor ikony Win32 (.ico) pro + výchozí prostředky Win32. +-win32manifest:<file> Zadaný soubor je vložený do oddílu + manifestu výstupních dat PE. +-win32resource:<file> Určuje soubor prostředku Win32 (.res). - - CODE GENERATION - --optimize[+|-] Enable optimizations. --removeintchecks[+|-] Remove integer checks. Default off. --debug[+|-] Emit debugging information. --debug:full Emit full debugging information (default). --debug:pdbonly Emit full debugging information. --debug:portable Emit cross-platform debugging information. --debug:embedded Emit cross-platform debugging information into - the target .dll or .exe. --deterministic Produce a deterministic assembly - (including module version GUID and timestamp) --refonly Produce a reference assembly in place of the main output --instrument:TestCoverage Produce an assembly instrumented to collect - coverage information --sourcelink:<file> Source link info to embed into PDB. + - GENEROVÁNÍ KÓDU - +-optimize[+|-] Povolí optimalizace. +-removeintchecks[+|-] Odebere kontroly dat typu integer. Výchozí hodnota je vypnutá. +-debug[+|-] Vygeneruje ladicí informace. +-debug:full Vygeneruje úplné ladicí informace (výchozí). +-debug:pdbonly Vygeneruje úplné ladicí informace. +-debug:portable Vygeneruje ladicí informace pro různé platformy. +-debug:embedded Vygeneruje ladicí informace pro různé platformy do + cílového souboru .dll nebo .exe. +-deterministic Vytvoří deterministické sestavení + (včetně verze modulu GUID a časového razítka) +-refonly Vytvoří referenční sestavení místo hlavního výstupu +-instrument:TestCoverage Vytvoří sestavení instrumentované ke shromažďování + informací o pokrytí +-sourcelink:<file> Informace o odkazu na zdroj, který se má vložit do souboru PDB. - - ERRORS AND WARNINGS - --nowarn Disable all warnings. --nowarn:<number_list> Disable a list of individual warnings. --warnaserror[+|-] Treat all warnings as errors. --warnaserror[+|-]:<number_list> Treat a list of warnings as errors. --ruleset:<file> Specify a ruleset file that disables specific - diagnostics. --errorlog:<file>[,version=<sarif_version>] - Specify a file to log all compiler and analyzer - diagnostics in SARIF format. - sarif_version:{1|2|2.1} Default is 1. 2 and 2.1 - both mean SARIF version 2.1.0. --reportanalyzer Report additional analyzer information, such as - execution time. --skipanalyzers[+|-] Skip execution of diagnostic analyzers. + – CHYBY A UPOZORNĚNÍ - +-nowarn Zakáže všechna upozornění. +-nowarn:<number_list> Zakáže seznam jednotlivých upozornění. +-warnaserror[+|-] Považuje všechna upozornění za chyby. +-warnaserror[+|-]:<number_list> Považuje seznam upozornění za chyby. +-ruleset:<file> Určuje soubor sady pravidel, který zakáže konkrétní + diagnostiku. +-errorlog:<file>[,verze=<sarif_version>] + Určuje soubor k protokolování veškeré diagnostiky kompilátorů + a analyzátorů ve formátu SARIF. + sarif_version:{1|2|2.1} Výchozí hodnota je 1. 2 a 2.1 + obě znamenají SARIF verze 2.1.0. +-reportanalyzer Oznámí další informace o analyzátoru, například + čas spuštění. +-skipanalyzers[+|-] Přeskočí spuštění diagnostických analyzátorů. - - LANGUAGE - --define:<symbol_list> Declare global conditional compilation - symbol(s). symbol_list:name=value,... - (Short form: -d) --imports:<import_list> Declare global Imports for namespaces in - referenced metadata files. + - JAZYK - +-define:<symbol_list> Deklaruje globální podmíněnou kompilaci + symbol(y). symbol_list:name=value,... + (Krátký tvar: -d) +-imports:<import_list> Deklaruje globální importy pro obory názvů + v odkazovaných souborech metadat. import_list:namespace,... --langversion:? Display the allowed values for language version --langversion:<string> Specify language version such as - `default` (latest major version), or - `latest` (latest version, including minor versions), - or specific versions like `14` or `15.3` --optionexplicit[+|-] Require explicit declaration of variables. --optioninfer[+|-] Allow type inference of variables. --rootnamespace:<string> Specifies the root Namespace for all type - declarations. --optionstrict[+|-] Enforce strict language semantics. --optionstrict:custom Warn when strict language semantics are not - respected. --optioncompare:binary Specifies binary-style string comparisons. - This is the default. --optioncompare:text Specifies text-style string comparisons. +-langversion:? Zobrazí povolené hodnoty pro verzi jazyka +-langversion:<string> Určuje verzi jazyka, například + `default` (nejnovější hlavní verze) nebo + `latest` (nejnovější verze včetně podverzí), + případně konkrétní verze jako `14` nebo `15.3` +-optionexplicit[+|-] Vyžaduje explicitní deklaraci proměnných. +-optioninfer[+|-] Povolí odvozování typů proměnných. +-rootnamespace:<string> Určuje kořenový obor názvů pro všechny deklarace + typů. +-optionstrict[+|-] Vynutí striktní sémantiku jazyka. +-optionstrict:custom Zobrazí upozornění, pokud se nerespektuje striktní sémantika + jazyka. +-optioncompare:binary Určuje binární porovnávání řetězců. + Toto nastavení je výchozí. +-optioncompare:text Určuje textové porovnávání řetězců. - - MISCELLANEOUS - --help Display this usage message. (Short form: -?) --noconfig Do not auto-include VBC.RSP file. --nologo Do not display compiler copyright banner. --quiet Quiet output mode. --verbose Display verbose messages. --parallel[+|-] Concurrent build. --version Display the compiler version number and exit. + - RŮZNÉ - +-help Zobrazí tuto zprávu o využití. (Krátký tvar: -?) +-noconfig Nezahrne automaticky soubor VBC.RSP. +-nologo Nezobrazí hlavičku kompilátoru s uvedením autorských práv. +-quiet Bezobslužný režim výstupu. +-verbose Zobrazí podrobné zprávy. +-parallel[+|-] Souběžné sestavení. +-version Zobrazí číslo verze kompilátoru a ukončí se. - - ADVANCED - --baseaddress:<number> The base address for a library or module + - ROZŠÍŘENÉ - +-baseaddress:<number> Základní adresa knihovny nebo modulu (hex). --checksumalgorithm:<alg> Specify algorithm for calculating source file - checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). --codepage:<number> Specifies the codepage to use when opening - source files. --delaysign[+|-] Delay-sign the assembly using only the public - portion of the strong name key. --publicsign[+|-] Public-sign the assembly using only the public - portion of the strong name key. --errorreport:<string> Specifies how to handle internal compiler - errors; must be prompt, send, none, or queue - (default). --generatedfilesout:<dir> Place files generated during compilation in the - specified directory. --filealign:<number> Specify the alignment used for output file - sections. --highentropyva[+|-] Enable high-entropy ASLR. --keycontainer:<string> Specifies a strong name key container. --keyfile:<file> Specifies a strong name key file. --libpath:<path_list> List of directories to search for metadata - references. (Semi-colon delimited.) --main:<class> Specifies the Class or Module that contains - Sub Main. It can also be a Class that - inherits from System.Windows.Forms.Form. - (Short form: -m) --moduleassemblyname:<string> Name of the assembly which this module will - be a part of. --netcf Target the .NET Compact Framework. --nostdlib Do not reference standard libraries - (system.dll and VBC.RSP file). +-checksumalgorithm:<alg> Určuje algoritmus pro výpočet kontrolního součtu + zdrojového souboru uloženého v souboru PDB. Podporované hodnoty: + SHA1, SHA256 (výchozí), SHA384 nebo SHA512. +-codepage:<number> Určuje znakovou stránku, která se má použít při otevírání + zdrojových souborů. +-delaysign[+|-] Zpožděné podepsání sestavení pouze pomocí veřejné + část klíče silného názvu. +-delaysign[+|-] Veřejné podepsání sestavení pouze pomocí veřejné + část klíče silného názvu. +-errorreport:<string> Určuje, jak zpracovávat interní chyby + kompilátoru; musí to být výzva, odeslání, žádné nebo fronta + (výchozí). +-generatedfilesout:<dir> Umístí soubory vygenerované během kompilace + do zadaného adresáře. +-filealign:<number> Určuje zarovnání použité pro oddíly výstupního + souboru. +-highentropyva[+|-] Povolí technologii ASLR s vysokou entropií. +-keycontainer:<string> Určuje kontejner klíče se silným názvem. +-keyfile:<file> Určuje soubor klíče se silným názvem. +-libpath:<path_list> Seznam adresářů pro hledání odkazů + na metadata. (Položky oddělené středníkem.) +-main:<class> Určuje třídu nebo modul, který obsahuje + proceduru Sub Main. Může to být také třída, která + dědí z System.Windows.Forms.Form. + (Krátký tvar: -m) +-moduleassemblyname:<string> Název sestavení, jehož součástí bude + tento modul. +-netcf Cílem je platforma .NET Compact Framework. +-nostdlib Neodkazuje na standardní knihovny + (system.dll a soubor VBC.RSP). -pathmap:<K1>=<V1>,<K2>=<V2>,... - Specify a mapping for source path names output by - the compiler. --platform:<string> Limit which platforms this code can run on; - must be x86, x64, Itanium, arm, arm64 - AnyCPU32BitPreferred or anycpu (default). --preferreduilang Specify the preferred output language name. --nosdkpath Disable searching the default SDK path for standard library assemblies. --reportivts[+|-] Output information on all IVTs granted to this - assembly by all dependencies, and annotate foreign assembly - accessibility errors with what assembly they came from. --sdkpath:<path> Path used to search for standard library assemblies. --subsystemversion:<version> Specify subsystem version of the output PE. - version:<number>[.<number>] --utf8output[+|-] Emit compiler output in UTF-8 character - encoding. -@<file> Insert command-line settings from a text file --vbruntime[+|-|*] Compile with/without the default Visual Basic - runtime. --vbruntime:<file> Compile with the alternate Visual Basic - runtime in <file>. + Určuje mapování pro výstup názvů zdrojových cest podle + kompilátoru. +-platform:<string> Omezuje platformy, na kterých se může tento kód spouštět; + musí to být x86, x64, Itanium, arm, arm64 + AnyCPU32BitPreferred nebo anycpu (výchozí). +-preferreduilang Určuje preferovaný název výstupního jazyka. +-nosdkpath Zakáže vyhledávání výchozí cesty sady SDK pro standardní sestavení knihovny. +-reportivts[+|-] Výstupní informace o všech IVT udělených tomuto + sestavení podle všech závislostí a opatřit poznámkami cizí sestavení + chyby přístupnosti s tím, z jakého sestavení pocházejí. +-sdkpath:<path> Cesta použitá k vyhledání standardních sestavení knihovny. +-subsystemversion:<version> Určuje verzi subsystému výstupních dat PE. + verze:<number>[.<number>] +-utf8output[+|-] Vygeneruje výstup kompilátoru ve znaku UTF-8 + UTF-8. +@<file> Vloží nastavení příkazového řádku z textového souboru +-vbruntime[+|-|*] Kompiluje s výchozím modulem runtime Visual Basic nebo + bez něj. +-vbruntime:<file> Kompiluje s alternativním modulem runtime Visual Basic + v souboru <file>. diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.de.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.de.xlf index 5f4df8519992..5a792ca2de21 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.de.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.de.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -430,7 +430,7 @@ (Kurzform: -linkres) -nowin32manifest Das Standardmanifest sollte nicht im Manifestabschnitt der Ausgabe-PE eingebettet werden. --resource:<resinfo> Fügt die angegebene Datei als eingebettete +-resource:<resinfo> Fügt die angegebene Datei als eingebettete Assemblyressource hinzu. resinfo:<file>[,<name>[,public|private]] (Kurzform: -res) @@ -481,9 +481,9 @@ import_list:namespace,... -langversion:? Anzeigen der zulässigen Werte für die Sprachversion -langversion:<string> Sprachversion angeben, z. B. - „default“ (neueste Hauptversion) oder - „latest“ (neueste Version, einschließlich Nebenversionen), - oder bestimmte Versionen wie „14“ oder „15.3“ + `default` (neueste Hauptversion) oder + `latest` (neueste Version, einschließlich Nebenversionen), + oder bestimmte Versionen wie `14` oder `15.3` -optionexplicit[+|-] Erfordert eine explizite Deklaration von Variablen. -optioninfer[+|-] Typrückschluss von Variablen zulassen. -rootnamespace:<string> Gibt den Stammnamespace für alle @@ -509,7 +509,7 @@ (hex). -checksumalgorithm:<alg> Gibt einen Algorithmus zum Berechnen der Prüfsumme-Quelldatei an, die in PDB gespeichert ist. Folgende Werte werden unterstützt: - SHA1 oder SHA256 (Standard). + SHA-1, SHA-256 (Standard), SHA-384 oder SHA-512. -codepage:<number> Gibt die Codepage an, die beim Öffnen von Quelldateien verwendet werden soll. -delaysign[+|-] Verzögertes Signieren der Assembly nur mit dem öffentlichen diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.es.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.es.xlf index 4944d4f581c7..44fcac0e84e8 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.es.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.es.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -411,34 +411,34 @@ - ARCHIVOS DE ENTRADA - -addmodule:<file_list> Hace referencia a los metadatos de los módulos especificados -link:<file_list> Insertar metadatos en el ensamblado especificado en la - interoperabilidad. (Forma corta: -l) + especificado. (Forma corta: -l) -recurse:<wildcard> Incluir todos los archivos del directorio actual y subdirectorios según las especificaciones de caracteres comodín. --reference:<file_list> Hacer referencia a los metadatos del ensamblado - especificado. (Forma corta: -r) --analyzer:<file_list> Ejecutar los analizadores desde este ensamblado +-reference:<file_list> Metadatos de referencia del archivo o los archivos de ensamblado especificados + ensamblado. (Forma corta: -r) +-analyzer:<file_list> Ejecute los analizadores desde este ensamblado (Forma corta: -a) --additionalfile:<file list> Archivos adicionales que no afectan directamente a la generación - de código, pero los analizadores pueden usarse para producir +-additionalfile:<file list> Archivos adicionales que no afectan directamente al código + generación, pero los analizadores pueden usarse para producir errores o advertencias. - RECURSOS - --linkresource:<resinfo> Vincula el archivo especificado como un recurso de ensamblado - externo. +-linkresource:<resinfo> Vincula el archivo especificado como uno externo + recurso de ensamblado. resinfo:<file>[,<name>[,public|private]] (Forma corta: -linkres) -nowin32manifest El manifiesto predeterminado no debe incrustarse en la sección de manifiesto del PE de salida. --resource:<resinfo> Agrega el archivo especificado como un recurso de ensamblado - insertado. +-resource:<resinfo> Agrega el archivo especificado como insertado + recurso de ensamblado. resinfo:<file>[,<name>[,public|private]] (Forma corta: -res) -win32icon:<file> Especifica un archivo de icono de Win32 (.ico) para los recursos de Win32 predeterminados. --win32manifest:<file> El archivo proporcionado se incrusta en la sección de - manifiesto del PE de salida. --win32resource:<file> Especifica un archivo de recursos Win32 (.res). +-win32manifest:<file> El archivo proporcionado se incrusta en el manifiesto + sección del PE de salida. +-win32resource:<file> Especifica el archivo de recursos Win32 (.res). - GENERACIÓN DE CÓDIGO - -optimize[+|-] Habilitar optimizaciones. @@ -461,39 +461,39 @@ -nowarn:<number_list> Deshabilitar una lista de advertencias individuales. -warnaserror[+|-] Tratar todas las advertencias como errores. -warnaserror[+|-]:<number_list> Tratar una lista de advertencias como errores. --ruleset:<file> Especificar un archivo de conjunto de reglas que deshabilite diagnósticos - específicos. +-ruleset:<file> Especifique un archivo de conjunto de reglas que deshabilite específicos + diagnósticos. -errorlog:<file>[,version=<sarif_version>] - Especificar un archivo para registrar todos los diagnósticos del compilador y el analizador - en formato SARIF. - sarif_version:{1|2|2.1} El valor predeterminado es 1.2 y 2.1; - ambos hacen referencia a la versión 2.1.0 de SARIF. + Especifique un archivo para registrar todo lo del compilador y el analizador + diagnósticos en formato SARIF. + sarif_version:{1|2|2.1} El valor predeterminado es 1. 2 and 2.1 + ambos referencian a la versión 2.1.0 media de SARIF. -reportanalyzer Informar información adicional del analizador, como hora de ejecución. --skipanalyzers[+|-] Omitir la ejecución de los analizadores de diagnóstico. +-skipanalyzers[+|-] Omitir la ejecución de analizadores de diagnóstico. - LENGUAJE - --define:<symbol_list> Declarar símbolos de compilación condicional global. - symbol_list:name=value,... +-define:<symbol_list> Declarar compilación condicional global + símbolos. symbol_list:name=value,... (Forma corta: -d) -imports:<import_list> Declarar importaciones globales para espacios de nombres en - los archivos de metadatos a los que se hace referencia. + archivos de metadatos de referencia. import_list:namespace,... -langversion:? Mostrar los valores permitidos para la versión del lenguaje -langversion:<string> Especificar la versión de idioma como - "predeterminado" (versión principal más reciente) o - "más reciente" (versión más reciente, incluidas las versiones secundarias), - o versiones específicas, como "14" o "15.3" + `default` (versión principal más reciente) o + `latest` (versión más reciente, incluidas las versiones secundarias), + o versiones específicas, como `14` o `15.3` -optionexplicit[+|-] Requerir la declaración explícita de variables. -optioninfer[+|-] Permitir inferencia de tipos de variables. --rootnamespace:<string> Especificar el espacio de nombres de la raíz de todoas las declaraciones - de tipos. +-rootnamespace:<string> Especificar el espacio de nombres de la raíz de todos los tipos + declaraciones. -optionstrict[+|-] Aplicar una semántica de lenguaje estricta. -optionstrict:custom Advertir cuando la semántica estricta del lenguaje no sea respetada. -optioncompare:binary Especificar comparaciones de cadenas de estilo binario. Este es el valor predeterminado. --optioncompare:text Especificar comparaciones de cadenas de estilo de texto. +-optioncompare:text Especifica comparaciones de cadenas de estilo binario. - VARIOS - -help Mostrar este mensaje de uso. (Forma corta: -?) @@ -507,57 +507,57 @@ - AVANZADO - -baseaddress:<number> Dirección base de la biblioteca o módulo (hex). --checksumalgorithm:<alg> Especificar el algoritmo para calcular la suma de comprobación del archivo de origen - almacenada en PDB. Los valores admitidos son: - SHA1 o SHA256 (valor predeterminado). +-checksumalgorithm:<alg> Especifique el algoritmo para calcular el archivo de origen + suma de comprobación almacenada en PDB. Los valores admitidos son: + SHA1, SHA256 (predeterminado), SHA384 o SHA512. -codepage:<number> Especifica la página de códigos que se vaya a utilizar al abrir - archivos de código fuente. --delaysign[+|-] Retrasar la firma del ensamblado usando solo la porción - pública de la clave de nombre seguro. --publicsign[+|-] Firmar públicamente el ensamblado usando solo la porción - pública de la clave de nombre seguro. --errorreport:<string> Especifica cómo controlar los errores del compilador interno; - debe ser solicitar, enviar, ninguno o en cola + archivos de origen. +-delaysign[+|-] Retrasar la firma del ensamblado usando solo el público + parte de la clave de nombre seguro. +-publicsign[+|-] Firmar públicamente el ensamblado usando solo el público + parte de la clave de nombre seguro. +-errorreport:<string> Especifica cómo controlar el compilador interno + errores; debe ser solicitar, enviar, ninguno o en cola (predeterminado). --generatedfilesout:<dir> Colocar los archivos generados durante la compilación en el +-generatedfilesout:<dir> Colocar archivos generados durante la compilación en el directorio especificado. --filealign:<number> Especificar la alineación utilizada por las secciones de archivos - de salida. +-filealign:<number> Especificar la alineación utilizada por los archivos de salida + secciones. -highentropyva[+|-] Habilitar ASLR de alta entropía. -keycontainer:<string> Especifica un contenedor de claves de nombre seguro. -keyfile:<file> Especifica un archivo de clave de nombre seguro. --libpath:<path_list> Lista de directorios para buscar referencias a - metadatos. (Delimitado por punto y coma). +-libpath:<path_list> Lista de directorios para buscar metadatos + referencias. (Delimitado por punto y coma.) -main:<clase> Especifica la clase o el módulo que contenga - el subelemento principal. También puede ser una clase que + El subelemento principal. También puede ser una clase que hereda de System.Windows.Forms.Form. (Forma corta: -m) -moduleassemblyname:<string> Nombre del ensamblado del que este módulo formará parte. --netcf Destino de .NET Compact Framework. +-netcf Destino de .NET Compact Framework. -nostdlib No hacer referencia a bibliotecas estándar (archivo system.dll y VBC.RSP). -pathmap:<K1>=<V1>,<K2>=<V2>,... - Especificar una asignación para los nombres de ruta de acceso de origen generados por + Especifique una asignación para los nombres de ruta de acceso de origen generados por el compilador. -platform:<string> Limitar en qué plataformas se puede ejecutar este código; debe ser x86, x64, Itanium, arm, arm64 AnyCPU32BitPreferred o anycpu (valor predeterminado). --preferreduilang Especificar el nombre del lenguaje de salida preferido. --nosdkpath Deshabilitar la búsqueda de rutas de acceso SDK predeterminadas para los ensamblados de biblioteca estándar. +-preferreduilang Especifique el nombre del idioma de salida preferido. +-nosdkpath Deshabilita la búsqueda de rutas de acceso SDK predeterminadas para la biblioteca estándar. -reportivts[+|-] Información de salida sobre todos los IVT concedidos a este ensamblado por todas las dependencias y anotar los errores de accesibilidad de ensamblados externos con el ensamblado del que proceden. -sdkpath:<path> Ruta de acceso usada para buscar ensamblados de biblioteca estándar. --subsystemversion:<version> Especificar la versión del subsistema del PE de salida. +-subsystemversion:<version> Especificar versión del subsistema del PE de salida. version:<number>[.<number>] --utf8output[+|-] Emitir salida del compilador en codificación de caracteres - UTF-8. +-utf8output[+|-] Emitir salida del compilador en caracteres UTF-8 + codificación. @<file> Insertar la configuración de línea de comando de un archivo de texto --vbruntime[+|-|*] Compilar con o sin el runtime de Visual Basic - predeterminado. --vbruntime:<file> Compilar con el runtime de Visual Basic - alternativo en <file>. +-vbruntime[+|-|*] Compilar con o sin el Visual Basic predeterminado + ejecución. +-vbruntime:<file> Compilar con el Visual Basic alternativo + tiempo de ejecución en <file>. diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.fr.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.fr.xlf index 890d3122a67a..cb95a19c4a4c 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.fr.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.fr.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -464,7 +464,7 @@ -ruleset:<file> Spécifier un fichier d’ensemble de règles qui désactive des diagnostics spécifiques. -errorlog:<file>[,version=<sarif_version>] - Spécifier un fichier pour consigner tous les diagnostics du compilateur et de l’analyseur + Spécifiez un fichier pour consigner tous les diagnostics du compilateur et de l’analyseur au format SARIF. sarif_version:{1|2|2.1} La valeur par défaut est 1. 2 et 2.1, les deux indiquant la version 2.1.0 de SARIF. @@ -509,7 +509,7 @@ (hex). -checksumalgorithm:<alg> Spécifier l’algorithme pour calculer la somme de contrôle du fichier source stocké dans la PDB. Les valeurs prises en charge sont les suivantes : - SHA1 ou SHA256 (par défaut). + SHA1, SHA256 (par défaut), SHA384 ou SHA512. -codepage:<number> Spécifie la page de code à utiliser lors de l’ouverture des fichiers sources. -delaysign[+|-] Retarder la signature de l’assembly en utilisant uniquement diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.it.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.it.xlf index 74dd1aa2a496..238de2083b5a 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.it.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.it.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -467,7 +467,7 @@ Consente di specificare un file in cui registrare tutte le diagnostiche del compilatore e dell'analizzatore in formato SARIF. sarif_version:{1|2|2.1} L'impostazione predefinita è 1. 2 e 2.1 - si riferiscono entrambi a SARIF versione 2.1.0. + e si riferiscono entrambi a SARIF versione 2.1.0. -reportanalyzer Restituisce informazioni aggiuntive dell'analizzatore, ad esempio il tempo di esecuzione. -skipanalyzers[+|-] Ignora l'esecuzione degli analizzatore diagnostici. @@ -481,9 +481,9 @@ import_list:namespace,... -langversion:? Visualizza i valori consentiti per la versione del linguaggio -langversion:<string> Consente di specificare la versione del linguaggio, ad esempio - 'default' (ultima versione principale) o - 'latest' (ultima versione che include versioni secondarie) - o versioni specifiche come '14' o '15.3' + `default` (ultima versione principale) o + `latest` (ultima versione che include versioni secondarie) + o versioni specifiche come `14` o `15.3` -optionexplicit[+|-] Richiede la dichiarazione esplicita delle variabili. -optioninfer[+|-] Consente l'inferenza del tipo delle variabili. -rootnamespace:<string> Consente di specificare lo spazio dei nomi radice per tutti i tipi @@ -509,11 +509,11 @@ (hex). -checksumalgorithm:<alg> Consente di specificare l'algoritmo per calcolare il checksum del file di origine archiviato nel file PDB. I valori supportati sono: - SHA1 o SHA256 (impostazione predefinita). + SHA1, SHA256 (valore predefinito), SHA384 e SHA512. -codepage:<number> Consente di specificare la tabella codici da usare all'apertura dei file di origine. -delaysign[+|-] Ritarda la firma dell'assembly usando solo la parte pubblica della - della chiave con nome sicuro. + chiave con nome sicuro. -publicsign[+|-] Firma pubblicamente l'assembly usando solo la parte pubblica della chiave con nome sicuro. -errorreport:<string> Consente di specificare come gestire gli errori interni del compilatore. @@ -527,7 +527,7 @@ -keycontainer:<string> Consente di specificare un contenitore di chiavi con nome sicuro. -keyfile:<file> Consente di specificare un file di chiave con nome sicuro. -libpath:<path_list> Elenco di directory in cui cercare - riferimenti ai metadati. (Delimitato da punto e virgola). + riferimenti ai metadati. (Delimitato da punto e virgola.) -main:<class> Consente di specificare la classe o il modulo che contiene Sub Main. Può essere anche una classe che eredita da System.Windows.Forms.Form. diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ja.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ja.xlf index 4287b7be8ccf..3b2379338ed1 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ja.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ja.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -411,7 +411,7 @@ - 入力ファイル - -addmodule:<file_list> 指定されたモジュールからのメタデータを参照します -link:<file_list> 指定された相互運用アセンブリからのメタデータを - 参照します。(短い形式: -l) + 埋め込みます。(短い形式: -l) -recurse:<wildcard> ワイルドカードの仕様に従って、現在のディレクトリと サブディレクトリ内のすべての ファイルを含めます。 @@ -425,7 +425,7 @@ - リソース - -linkresource:<resinfo> 指定されたファイルを外部アセンブリ リソースとして - として追加します。 + リンクします。 resinfo:<file>[,<name>[,public|private]] (短い形式: -linkres) -nowin32manifest 既定のマニフェストを、出力 PE のマニフェスト セクション @@ -445,10 +445,10 @@ -removeintchecks[+|-] 整数チェックを削除します。既定値はオフです。 -debug[+|-] デバッグ情報を出力します。 -debug:full 完全なデバッグ情報を出力します (既定)。 --debug:pdbonly 完全なデバッグ情報を出力します。 --debug:portable クロスプラットフォーム デバッグ情報を出力します。 +-debug:pdbonly 完全なデバッグ情報を生成します。 +-debug:portable クロスプラットフォーム デバッグ情報を生成します。 -debug:embedded クロスプラットフォームのデバッグ情報を、ターゲットの .dll または .exe に - 出力します。 + 生成ます。 -deterministic 決定論的アセンブリ を生成します (モジュール バージョン GUID とタイムスタンプを含む) @@ -482,8 +482,8 @@ import_list:namespace,... -langversion:? 言語バージョンに指定できる値を表示します -langversion:<string> 次のような言語バージョンを指定します。 - 'default' (最新のメジャー バージョン)、または - 'latest' (マイナー バージョンを含む最新バージョン)、 + `default` (最新のメジャー バージョン)、または + `latest` (マイナー バージョンを含む最新バージョン)、 または `14` や `15.3` などの特定のバージョン -optionexplicit[+|-] 変数の明示的な宣言が必要です。 -optioninfer[+|-] 変数の型推論を許可します。 @@ -510,7 +510,7 @@ (16 進数)。 -checksumalgorithm:<alg> PDB に格納されているソース ファイルのチェックサムを計算するためのアルゴリズムを 指定します。サポートされている値: - SHA1 または SHA256 (既定値)。 + SHA1、SHA256 (既定)、SHA384、または SHA512。 -codepage:<number> ソース ファイルを開くときに使用するコードページを 指定します。 -delaysign[+|-] 厳密な名前キーの公開部分のみを使用して @@ -553,7 +553,7 @@ -subsystemversion:<version> 出力 PE のサブシステム バージョンを指定します。 version:<number>[.<number>] -utf8output[+|-] UTF-8 文字エンコードでコンパイラ出力を - 作成します。 + 生成します。 @<file> テキスト ファイルからコマンド ライン設定を挿入します -vbruntime[+|-|*] コンパイル時に既定の Visual Basic ランタイムを使用するかどうかを 指定します。 diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ko.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ko.xlf index fac56d9b279a..7b5a43954982 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ko.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ko.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -411,7 +411,7 @@ - 입력 파일 - -addmodule:<file_list> 지정된 모듈의 참조 메타데이터 -link:<file_list> 지정한 interop 어셈블리의 메타데이터를 - 포함합니다. (약식: -l) + 참조합니다. (약식: -l) -recurse:<wildcard> 현재 디렉터리 및 하위 디렉터리의 모든 파일을 와일드카드 사양에 따라 포함합니다. @@ -441,7 +441,7 @@ -win32resource:<file> Win32 리소스 파일(.res)을 지정합니다. - 코드 생성 - --optimize[+|-] 최적화를 사용하도록 설정합니다. +-optimize[+|-] 최적화를 활성화합니다. -removeintchecks[+|-] 정수 검사를 제거합니다. 기본값은 꺼짐입니다. -debug[+|-] 디버깅 정보를 내보냅니다. -debug:full 전체 디버깅 정보를 내보냅니다(기본값). @@ -452,16 +452,16 @@ -deterministic 결정적 어셈블리 생성 (모듈 버전 GUID 및 타임스탬프 포함) -refonly 주 출력 대신 참조 어셈블리 생성 --instrument:TestCoverage 검사 정보를 수집하는 데 사용될 +-instrument:TestCoverage 적용 범위 정보를 수집하는 데 사용될 어셈블리 생성 -sourcelink:<file> PDB에 포함할 원본 링크 정보입니다. - 오류 및 경고 - --nowarn 모든 경고를 사용하지 않도록 설정합니다. --nowarn:<number_list> 개별 경고 목록을 사용하지 않도록 설정합니다. +-nowarn 모든 경고를 비활성화합니다. +-nowarn:<number_list> 개별 경고 목록을 비활성화합니다. -warnaserror[+|-] 모든 경고를 오류로 처리합니다. -warnaserror[+|-]:<number_list> 경고 목록을 오류로 처리합니다. --ruleset:<file> 특정 진단을 사용하지 않도록 설정하는 규칙 집합 파일을 +-ruleset:<file> 특정 진단을 비활성화하는 규칙 집합 파일을 지정합니다. -errorlog:<file>[,version=<sarif_version>] 모든 컴파일러 및 분석기의 진단을 SARIF 형식으로 기록할 @@ -481,9 +481,9 @@ import_list:namespace,... -langversion:? 언어 버전에 허용되는 값 표시 -langversion:<string> 다음과 같은 언어 버전 지정 - 'default'(최신 주 버전), - 'latest'(부 버전을 포함한 최신 버전), - 또는 특정 버전(예: '14' 또는 '15.3') + `default`(최신 주 버전), + `latest`(부 버전을 포함한 최신 버전), + 또는 특정 버전(예: `14` 또는 `15.3`) -optionexplicit[+|-] 변수의 명시적 선언이 필요합니다. -optioninfer[+|-] 변수의 형식 유추를 허용합니다. -rootnamespace:<string> 모든 형식 선언에 대한 루트 네임스페이스를 @@ -509,7 +509,7 @@ (16진수). -checksumalgorithm:<alg> PBD에 저장된 소스 파일 체크섬을 계산하기 위한 알고리즘을 지정합니다. 지원되는 값은 - SHA1 또는 SHA256(기본값)입니다. + SHA-1, SHA256(기본값), SHA384 또는 SHA512입니다. -codepage:<number> 소스 파일을 열 때 사용할 코드 페이지를 지정합니다. -delaysign[+|-] 강력한 이름의 키의 공개 부분만 사용하여 어셈블리 서명을 @@ -523,7 +523,7 @@ 이동합니다. -filealign:<number> 출력 파일 섹션에 사용되는 맞춤을 지정합니다. --highentropyva[+|-] 높은 엔트로피 ASLR을 사용하도록 설정합니다. +-highentropyva[+|-] 높은 엔트로피 ASLR을 활성화합니다. -keycontainer:<string> 강력한 이름의 키 컨테이너를 지정합니다. -keyfile:<file> 강력한 이름의 키 파일을 지정합니다. -libpath:<path_list> 메타데이터 참조를 검색할 디렉터리 @@ -544,7 +544,7 @@ x86, x64, Itanium, arm, arm64 AnyCPU32BitPreferred 또는 anycpu(기본값)이어야 합니다. -preferreduilang 기본 출력 언어 이름을 지정합니다. --nosdkpath 표준 라이브러리 어셈블리의 기본 SDK 경로 검색을 사용하지 않도록 설정합니다. +-nosdkpath 표준 라이브러리 어셈블리의 기본 SDK 경로 검색을 비활성화합니다. -reportivts[+|-] 모든 종속 항목에 의한 이 어셈블리에 부여된 모든 IVT에 대한 모든 종속성에 의한 어셈블리 및 외부 어셈블리 접근성 오류가 발생한 어셈블리에 대한 주석을 추가합니다. diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.pl.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.pl.xlf index cd46fb62144b..71312949d944 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.pl.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.pl.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -474,16 +474,16 @@ - JĘZYK - -define:<symbol_list> Deklarowanie globalnych symboli kompilacji - warunkowej. symbol_list:name=value,... + symbol_list:name=value,... (skrócona postać: -d) -imports:<import_list> Deklarowanie globalnych importów dla przestrzeni nazw w przywoływanych plikach metadanych. import_list:namespace,... -langversion:? Wyświetlanie dozwolonych wartości dla wersji językowej -langversion:<string> Określanie wersji językowej, na przykład - „default” (najnowsza wersja główna) lub - „latest” (najnowsza wersja, w tym wersje pomocnicze), - albo dokładnych wersji, takich jak „14” lub „15.3” + `default` (najnowsza wersja główna) lub + `latest` (najnowsza wersja, w tym wersje pomocnicze), + albo dokładnych wersji, takich jak `14` lub `15.3` -optionexplicit[+|-] Wymaganie jawnej deklaracji zmiennych. -optioninfer[+|-] Zezwalanie na wnioskowanie o typie zmiennych. -rootnamespace:<string> Określanie głównej przestrzeni nazw dla wszystkich deklaracji @@ -509,7 +509,7 @@ (szestnastkowy). -checksumalgorithm:<alg> Określanie algorytmu obliczania sumy kontrolnej pliku źródłowego przechowywanego w pliku PDB. Obsługiwane wartości to: - SHA1 lub SHA256 (domyślna). + SHA1, SHA256 (domyślne), SHA384, or SHA512. -codepage:<number> Określanie strony kodowej do użycia podczas otwierania plików źródłowych. -delaysign[+|-] Podpisywanie z opóźnieniem zestawu tylko przy użyciu publicznego diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.pt-BR.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.pt-BR.xlf index 5c1afed4a57d..c49d137f2bf5 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.pt-BR.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.pt-BR.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -391,173 +391,173 @@ -vbruntime:<file> Compile with the alternate Visual Basic runtime in <file>. - Visual Basic Compiler Options + Opções do Compilador do Visual Basic - - OUTPUT FILE - --out:<file> Specifies the output file name. --target:exe Create a console application (default). - (Short form: -t) --target:winexe Create a Windows application. --target:library Create a library assembly. --target:module Create a module that can be added to an + - ARQUIVO DE SAÍDA - +-out:<file> Especifica o nome do arquivo de saída. +-target:exe Criar um aplicativo de console (padrão). + (Forma abreviada: -t) +-target:winexe Criar um aplicativo do Windows. +-target:library Criar um assembly de biblioteca. +-target:module Criar um módulo que pode ser adicionado a um assembly. --target:appcontainerexe Create a Windows application that runs in +-target:appcontainerexe Criar um aplicativo do Windows que seja executado em AppContainer. --target:winmdobj Create a Windows Metadata intermediate file --doc[+|-] Generates XML documentation file. --doc:<file> Generates XML documentation file to <file>. --refout:<file> Reference assembly output to generate +-target:winmdobj Criar um arquivo intermediário de Metadados do Windows +-doc[+|-] Gera arquivo de documentação XML. +-doc:<file> Gera arquivo de documentação XML para <file>. +-refout:<file> Saída de montagem de referência para gerar - - INPUT FILES - --addmodule:<file_list> Reference metadata from the specified modules --link:<file_list> Embed metadata from the specified interop - assembly. (Short form: -l) --recurse:<wildcard> Include all files in the current directory - and subdirectories according to the - wildcard specifications. --reference:<file_list> Reference metadata from the specified - assembly. (Short form: -r) --analyzer:<file_list> Run the analyzers from this assembly - (Short form: -a) --additionalfile:<file list> Additional files that don't directly affect code - generation but may be used by analyzers for producing - errors or warnings. + - ARQUIVOS DE ENTRADA - +-addmodule:<file_list> Metadados de referência dos módulos especificados +-link:<file_list> Incorporar metadados do assembly de + interoperabilidade especificada. (Forma abreviada: -l) +-recurse:<wildcard> Incluir todos os arquivos no diretório + e subdiretórios vtuais de acordo com as + especificações curinga. +-reference:<file_list> Metadados de referência do assembly + interoperabilidade especificada. (Forma abreviada: -r) +-analyzer:<file_list> Executar os analisadores dessa montagem + (Forma abreviada: -a) +-additionalfile:<file list> Arquivos adicionais que não afetam diretamente a geração + do código, mas pode ser usado por analisadores para produzir + erros ou avisos. - - RESOURCES - --linkresource:<resinfo> Links the specified file as an external - assembly resource. + - RECURSOS - +-linkresource:<resinfo> Vincula o arquivo especificado como um recurso de assembly + externo. resinfo:<file>[,<name>[,public|private]] - (Short form: -linkres) --nowin32manifest The default manifest should not be embedded - in the manifest section of the output PE. --resource:<resinfo> Adds the specified file as an embedded - assembly resource. + (Forma abreviada: -linkres) +-nowin32manifest O manifesto padrão não deve ser incorporado + na seção de manifesto do PE de saída. +-resource:<resinfo> Adiciona o arquivo especificado como um recurso + externo. resinfo:<file>[,<name>[,public|private]] - (Short form: -res) --win32icon:<file> Specifies a Win32 icon file (.ico) for the - default Win32 resources. --win32manifest:<file> The provided file is embedded in the manifest - section of the output PE. --win32resource:<file> Specifies a Win32 resource file (.res). + (Forma abreviada: -res) +-win32icon:<file> Especifica um arquivo de ícone Win32 (.ico) nos + recursos padrão do Win32. +-win32manifest:<file> O arquivo fornecido está incorporado na seção + do manifesto da saída PE. +-win32resource:<file> Especifica um arquivo de recurso Win32 (.res). - - CODE GENERATION - --optimize[+|-] Enable optimizations. --removeintchecks[+|-] Remove integer checks. Default off. --debug[+|-] Emit debugging information. --debug:full Emit full debugging information (default). --debug:pdbonly Emit full debugging information. --debug:portable Emit cross-platform debugging information. --debug:embedded Emit cross-platform debugging information into - the target .dll or .exe. --deterministic Produce a deterministic assembly - (including module version GUID and timestamp) --refonly Produce a reference assembly in place of the main output --instrument:TestCoverage Produce an assembly instrumented to collect - coverage information --sourcelink:<file> Source link info to embed into PDB. + - GERAÇÃO DE CÓDIGO - +-optimize[+|-] Habilitar otimizações. +-removeintchecks[+|-] Remover verificações de números inteiros. Padrão desativado. +-debug[+|-] Emitir informações de depuração. +-debug:full Emitir informações completas de depuração (padrão). +-debug:pdbonly Emitir informações completas de depuração. +-debug:portable Emitir informações de depuração entre plataformas. +-debug:embedded Emitir informações de depuração de plataforma cruzada no + .dll ou .exe de destino. +-deterministic Produzir uma assembly determinística + (incluindo o GUID da versão do módulo e carimbo de data/hora) +-refonly Produzir uma assembly de referência no lugar da saída principal +-instrument:TestCoverage Produzir uma assembly instrumentada para coletar + informações de cobertura +-sourcelink:<file> Informações do link de origem para incorporar no PDB. - - ERRORS AND WARNINGS - --nowarn Disable all warnings. --nowarn:<number_list> Disable a list of individual warnings. --warnaserror[+|-] Treat all warnings as errors. --warnaserror[+|-]:<number_list> Treat a list of warnings as errors. --ruleset:<file> Specify a ruleset file that disables specific - diagnostics. --errorlog:<file>[,version=<sarif_version>] - Specify a file to log all compiler and analyzer - diagnostics in SARIF format. - sarif_version:{1|2|2.1} Default is 1. 2 and 2.1 - both mean SARIF version 2.1.0. --reportanalyzer Report additional analyzer information, such as - execution time. --skipanalyzers[+|-] Skip execution of diagnostic analyzers. + - ERROS E AVISOS - +-nowarn Desabilitar todos os avisos. +-nowarn:<number_list> Desabilitar uma lista de avisos individuais. +-warnaserror[+|-] Tratar todos os avisos como erros. +-warnaserror[+|-]:<number_list> Tratar uma lista de avisos como erros. +-ruleset:<file> Especificar um arquivo de conjunto de regras que desabilita determinados + diagnósticos. +-errorlog:<file>[,versão=<sarif_version>] + Especificar um arquivo para registrar o diagnóstico de compilador e analisador + no formato SARIF. + sarif_version:{1|2|2.1} Padrão é 1. 2 e 2.1 + ambos significam versão SARIF 2.1.0. +-reportanalyzer Relatar informações adicionais do analisador, como + tempo de execução. +-skipanalyzers[+|-] Ignorar a execução de analisadores de diagnóstico. - - LANGUAGE - --define:<symbol_list> Declare global conditional compilation - symbol(s). symbol_list:name=value,... - (Short form: -d) --imports:<import_list> Declare global Imports for namespaces in - referenced metadata files. + - IDIOMA - +-define:<symbol_list> Declarar o(s) símbolo(s) da compilação + condicional global. symbol_list:name=value,... + (Forma abreviada: -d) +-imports:<import_list> Declarar as importações globais dos namespaces nos + arquivos de metadados referenciados. import_list:namespace,... --langversion:? Display the allowed values for language version --langversion:<string> Specify language version such as - `default` (latest major version), or - `latest` (latest version, including minor versions), - or specific versions like `14` or `15.3` --optionexplicit[+|-] Require explicit declaration of variables. --optioninfer[+|-] Allow type inference of variables. --rootnamespace:<string> Specifies the root Namespace for all type - declarations. --optionstrict[+|-] Enforce strict language semantics. --optionstrict:custom Warn when strict language semantics are not - respected. --optioncompare:binary Specifies binary-style string comparisons. - This is the default. --optioncompare:text Specifies text-style string comparisons. +-langversion:? Exibir os valores permitidos da versão do idioma +-langversion:<string> Especificar a versão do idioma, como + `default` (última versão principal), ou + `latest` (versão mais recente, incluindo versões secundárias), + ou versões específicas como `14` ou `15.3` +-optionexplicit[+|-] Requer declaração explícita de variáveis. +-optioninfer[+|-] Permitir a inferência de tipo de variáveis. +-rootnamespace:<string> Especifica o Namespace raiz para todos os tipos + de declarações. +-optionstrict[+|-] Aplicar a semântica de linguagem estrita. +-optionstrict:custom Avisar quando a semântica estrita da linguagem não for + respeitada. +-optioncompare:binary Especifica as comparações de cadeia de caracteres de estilo binário. + Esse é o padrão. +-optioncompare:text Especifica as comparações de cadeia de estilo de texto. - - MISCELLANEOUS - --help Display this usage message. (Short form: -?) --noconfig Do not auto-include VBC.RSP file. --nologo Do not display compiler copyright banner. --quiet Quiet output mode. --verbose Display verbose messages. --parallel[+|-] Concurrent build. --version Display the compiler version number and exit. + - DIVERSOS - +-help Exibir esta mensagem de uso. (Forma abreviada: -?) +-noconfig Não incluir automaticamente o arquivo VBC.RSP. +-nologo Não exibir o banner de direitos autorais do compilador. +-quiet Modo de saída silencioso. +-verbose Exibir as mensagens detalhadas. +-parallel[+|-] Compilação simultânea. +-version Exibir o número da versão do compilador e sair. - - ADVANCED - --baseaddress:<number> The base address for a library or module + - AVANÇADO - +-baseaddress:<number> O endereço base de uma biblioteca ou módulo (hex). --checksumalgorithm:<alg> Specify algorithm for calculating source file - checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). --codepage:<number> Specifies the codepage to use when opening - source files. --delaysign[+|-] Delay-sign the assembly using only the public - portion of the strong name key. --publicsign[+|-] Public-sign the assembly using only the public - portion of the strong name key. --errorreport:<string> Specifies how to handle internal compiler - errors; must be prompt, send, none, or queue - (default). --generatedfilesout:<dir> Place files generated during compilation in the - specified directory. --filealign:<number> Specify the alignment used for output file - sections. --highentropyva[+|-] Enable high-entropy ASLR. --keycontainer:<string> Specifies a strong name key container. --keyfile:<file> Specifies a strong name key file. --libpath:<path_list> List of directories to search for metadata - references. (Semi-colon delimited.) --main:<class> Specifies the Class or Module that contains - Sub Main. It can also be a Class that - inherits from System.Windows.Forms.Form. - (Short form: -m) --moduleassemblyname:<string> Name of the assembly which this module will - be a part of. --netcf Target the .NET Compact Framework. --nostdlib Do not reference standard libraries - (system.dll and VBC.RSP file). +-checksumalgorithm:<alg> Especificar o algoritmo para calcular a soma de verificação do arquivo de origem + soma de verificação armazenada no PDB. Os valores suportados são: + SHA1, SHA256 (padrão), SHA384 ou SHA512. +-codepage:<number> Especifica a página de código a ser usada ao abrir + os arquivos de origem. +-delaysign[+|-] Atrasar a assinatura do assembly usando apenas a parte do público + da chave de nome forte. +-publicsign[+|-] Assinar publicamente o assembly usando apenas a parte do público + da chave de nome forte. +-errorreport:<string> Especifica como lidar com os erros do compilador interno + deve ser prompt, enviar, nenhum ou fila + (padrão). +-generatedfilesout:<dir> Coloque os arquivos gerados durante a compilação no + diretório especificado. +-filealign:<number> Especificar o alinhamento usado nas seções + arquivo de saída. +-highentropyva[+|-] Habilitar o ASLR de alta entropia. +-keycontainer:<string> Especifica um contêiner de chave de nome forte. +-keyfile:<file> Especifica um arquivo de chave de nome forte. +-libpath:<path_list> Lista de diretórios para pesquisar pelas referências de + metadados. (Delimitado por ponto e vírgula.) +-main:<class> Especifica a Classe ou Módulo que contém + Sub Principal. Também pode ser uma Classe que + herda do System.Windows.Forms.Form. + (Forma abreviada: -m) +-moduleassemblyname:<string> Nome do assembly que este módulo fará + parte. +-netcf Direcionar o .NET Compact Framework. +-nostdlib Não referenciar as bibliotecas padrão + (arquivo system.dll e VBC.RSP). -pathmap:<K1>=<V1>,<K2>=<V2>,... - Specify a mapping for source path names output by - the compiler. --platform:<string> Limit which platforms this code can run on; - must be x86, x64, Itanium, arm, arm64 - AnyCPU32BitPreferred or anycpu (default). --preferreduilang Specify the preferred output language name. --nosdkpath Disable searching the default SDK path for standard library assemblies. --reportivts[+|-] Output information on all IVTs granted to this - assembly by all dependencies, and annotate foreign assembly - accessibility errors with what assembly they came from. --sdkpath:<path> Path used to search for standard library assemblies. --subsystemversion:<version> Specify subsystem version of the output PE. - version:<number>[.<number>] --utf8output[+|-] Emit compiler output in UTF-8 character - encoding. -@<file> Insert command-line settings from a text file --vbruntime[+|-|*] Compile with/without the default Visual Basic - runtime. --vbruntime:<file> Compile with the alternate Visual Basic - runtime in <file>. + Especificar um mapeamento da saída de nomes do caminho de origem pelo + compilador. +-platform:<string> Limitar em quais plataformas esse código pode ser executado; + precisa ser x86, x64, Itanium, arm, arm64 + AnyCPU32BitPreferred ou anycpu (padrão). +-preferreduilang Especificar o nome do idioma de saída preferido. +-nosdkpath Desabilite a pesquisa do caminho padrão do SDK nos conjuntos de bibliotecas padrão. +-reportivts[+|-] Informações de saída sobre todos os IVTs concedidos a esse + assembly por todas as dependências e anotar os erros de acessibilidade do assembly externo + com qual assembly eles vieram. +-sdkpath:<path> Caminho usado para pesquisar assemblies de biblioteca padrão. +-subsystemversion:<version> Especificar a versão do subsistema do PE de saída. + versão:<number>[.<number>] +-utf8output[+|-] Emitir a saída do compilador na codificação do + caractere UTF-8. +@<file> Inserir as configurações de linha de comando de um arquivo de texto +-vbruntime[+|-|*] Compilar com/sem o tempo de execução do + Visual Basic padrão. +-vbruntime:<file> Compilar com o tempo de execução do + Visual Basic alternativo em <file>. diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ru.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ru.xlf index 467772e1411a..b3e0f2496737 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.ru.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.ru.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -393,7 +393,7 @@ Параметры компилятора Visual Basic - — ВЫХОДНОЙ ФАЙЛ — + - ВЫХОДНОЙ ФАЙЛ - -out:<файл> Задает имя выходного файла. -target:exe Создать консольное приложение (по умолчанию). (Краткая форма: -t) @@ -408,10 +408,10 @@ -doc:<файл> Создает XML-файл документации <file>. -refout:<файл> Указать выходные данные сборки для создания - — ВХОДНЫЕ ФАЙЛЫ — + - ВХОДНЫЕ ФАЙЛЫ - -addmodule:<список_файлов> Указать метаданные из заданных модулей -link:<список_файлов> Внедрять метаданные из указанной сборки - межпрограммного взаимодействия. (Краткая форма: -l) + сборки. (Краткая форма: -l) -recurse:<подстановочный знак> Включить все файлы в текущем каталоге и подкаталогах в соответствии со спецификациями подстановочных знаков. @@ -423,7 +423,7 @@ но могут использоваться анализаторами для создания ошибок и предупреждений. - — РЕСУРСЫ — + - РЕСУРСЫ - -linkresource:<resinfo> Связывает указанный файл как внешний сборочный ресурс. resinfo:<файл>[,<имя>[,общедоступный|частный]] @@ -440,7 +440,7 @@ манифеста выходного PE. -win32resource:<файл> Задает файл ресурсов Win32 (.res). - — ГЕНЕРАЦИЯ КОДА — + - ГЕНЕРАЦИЯ КОДА - -optimize[+|-] Включить оптимизацию. -removeintchecks[+|-] Удалить целочисленные проверки. По умолчанию отключено. -debug[+|-] Выдавать отладочные сведения. @@ -456,7 +456,7 @@ сведений об охвате -sourcelink:<файл> Сведения об исходной ссылке для встраивания в PDB. - — ОШИБКИ И ПРЕДУПРЕЖДЕНИЯ — + - ОШИБКИ И ПРЕДУПРЕЖДЕНИЯ - -nowarn Отключить все предупреждения. -nowarn:<список_чисел> Отключить список индивидуальных предупреждений. -warnaserror[+|-] Обрабатывать все предупреждения как ошибки. @@ -472,7 +472,7 @@ время выполнения. -skipanalyzers[+|-] Пропустить выполнение диагностических анализаторов. - — ЯЗЫК — + - ЯЗЫК - -define:<список_символов> Объявить глобальные символы условной компиляции. symbol_list:name=value,... (Краткая форма: -d) @@ -481,21 +481,21 @@ import_list:namespace,... -langversion:? Показать допустимые значения для языковой версии -langversion:<строка> Указать версию языка, например - "по умолчанию" (последняя основная версия) + "по умолчанию" (последняя основная версия), "последняя" (последняя версия, включая дополнительные версии) или специальные версии, такие как "14" либо "15.3" -optionexplicit[+|-] Требовать явного объявления переменных. -optioninfer[+|-] Разрешить определение типа переменных. -rootnamespace:<строка> Указывает корневое пространство имен для всех типов деклараций. -optionstrict[+|-] Принудительное применение строгой семантики языка. +-optionstrict[+|-] Принудительное применение строгой семантики языка. -optionstrict:custom Предупреждать, если строгая семантика не соблюдается. -optioncompare:binary Задает сравнение строк в двоичном формате. Это значение по умолчанию. -optioncompare:text Задает сравнение строк в виде текста. - — РАЗНОЕ — + - РАЗНОЕ - -help Отображать сообщение об использовании. (Краткая форма: -?) -noconfig Не включать в состав файл VBC.RSP автоматически. -nologo Не отображать баннер компилятора со сведениями об авторских правах. @@ -504,12 +504,12 @@ optionstrict[+|-] Принудительное применени -parallel[+|-] Параллельная сборка. -version Показать номер версии компилятора и выйти. - — ДОПОЛНИТЕЛЬНЫЕ ПАРАМЕТРЫ — + - ДОПОЛНИТЕЛЬНЫЕ ПАРАМЕТРЫ - -baseaddress:<число> Базовый адрес библиотеки или модуля (шестнадцатеричный). -checksumalgorithm:<alg> Указать алгоритм вычисления контрольной суммы исходного файла, хранящегося в PDB. Поддерживаемые значения: - SHA1 или SHA256 (по умолчанию). + SHA1, SHA256 (по умолчанию), SHA384 или SHA512. -codepage:<число> Указывает кодовую страницу, используемую при открытии исходных файлов. -delaysign[+|-] Отложенная подпись сборки с использованием только общедоступной diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.tr.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.tr.xlf index 208ed080cd9b..ae0958cfaf99 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.tr.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.tr.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -393,50 +393,50 @@ Visual Basic Derleyicisi Seçenekleri - - ÇIKIŞ DOSYASI - --out:<file> Çıkış dosyası adını belirtir. + - ÇIKTI DOSYASI - +-out:<file> Çıktı dosyası adını belirtir. -target:exe Konsol uygulaması oluşturur (varsayılan). (Kısa biçimi: -t) -target:winexe Windows uygulaması oluşturur. --target:library Kitaplık derlemesi oluşturur. --target:module Derlemeye eklenebilecek bir modül +-target:library Kitaplık bütünleştirilmiş kodu oluşturur. +-target:module Bütünleştirilmiş koda eklenebilecek bir modül oluşturur. --target:appcontainerexe AppContainer içinde çalışan bir Windows uygulaması +-target:appcontainerexe AppContainer’da çalışan bir Windows uygulaması oluşturur. -target:winmdobj Bir Windows Meta Veri ara dosyası oluşturur -doc[+|-] XML belgeleri dosyası oluşturur. -doc:<file> <file> için XML belgeleri dosyası oluşturur. --refout:<file> Oluşturulacak başvuru derlemesi çıkışı +-refout:<file> Oluşturulacak başvuru bütünleştirilmiş kodu çıktısı - GİRİŞ DOSYALARI - --addmodule:<file_list> Belirtilen modüllerdeki başvuru meta verileri --link:<file_list> Belirtilen birlikte çalışma derlemesinden ekleme - meta verileri. (Kısa biçimi: -l) +-addmodule:<file_list> Belirtilen modüllerdeki meta verilere başvurur +-link:<file_list> Belirtilen birlikte çalışma bütünleştirilmiş kodundan meta verileri + ekler. (Kısa biçimi: -l) -recurse:<wildcard> Joker karakter belirtimlerine göre geçerli dizindeki ve alt dizinlerdeki tüm dosyaları ekler. --reference:<file_list> Belirtilen derlemedeki başvuru - meta verileri. (Kısa biçimi: -r) --analyzer:<file_list> Bu derlemedeki çözümleyicileri çalıştırır +-reference:<file_list> Belirtilen bütünleştirilmiş koddaki meta verilere + başvurur. (Kısa biçimi: -r) +-analyzer:<file_list> Bu bütünleştirilmiş koddaki çözümleyicileri çalıştırır (Kısa biçimi: -a) -additionalfile:<file list> Kod oluşturmayı doğrudan etkilemeyen ancak hata veya uyarı üretimi için çözümleyiciler tarafından kullanılabilen ek dosyalar. - KAYNAKLAR - --linkresource:<resinfo> Belirtilen dosyayı bir dış derleme kaynağı +-linkresource:<resinfo> Belirtilen dosyayı bir dış bütünleştirilmiş kod kaynağı olarak bağlar. resinfo:<file>[,<name>[,public|private]] (Kısa biçimi: -linkres) --nowin32manifest Varsayılan bildirim çıkış PE’sinin bildirim bölümüne +-nowin32manifest Varsayılan bildirim çıktı PE’sinin bildirim bölümüne eklenmemeli. --resource:<resinfo> Belirtilen dosyayı ekli bir derleme kaynağı +-resource:<resinfo> Belirtilen dosyayı ekli bir bütünleştirilmiş kod kaynağı olarak ekler. resinfo:<file>[,<name>[,public|private]] (Kısa biçimi: -res) -win32icon:<file> Varsayılan Win32 kaynakları için bir Win32 simge dosyası (.ico) belirtir. --win32manifest:<file> Sağlanan dosya çıkış PE’sinin bildirim bölümüne +-win32manifest:<file> Sağlanan dosya çıktı PE’sinin bildirim bölümüne eklenir. -win32resource:<file> Bir Win32 kaynak dosyası (.res) belirtir. @@ -444,17 +444,17 @@ -optimize[+|-] İyileştirmeleri etkinleştirir. -removeintchecks[+|-] Tamsayı denetimlerini kaldırır. Varsayılan olarak kapalıdır. -debug[+|-] Hata ayıklama bilgilerini gösterir. --debug:full Hata ayıklama bilgilerini tam gösterir (varsayılan). --debug:pdbonly Hata ayıklama bilgilerini tam gösterir. --debug:portable Farklı platformlardaki hata ayıklama bilgilerini gösterir. --debug:embedded Farklı platformlardaki hata ayıklama bilgilerini hedef +-debug:full Tam hata ayıklama bilgilerini gösterir (varsayılan). +-debug:pdbonly Tam hata ayıklama bilgilerini gösterir. +-debug:portable Platformlar arası hata ayıklama bilgilerini gösterir. +-debug:embedded Platformlar arası hata ayıklama bilgilerini hedef .dll veya .exe dosyasında gösterir. --deterministic Belirlenimci bir derleme üretir +-deterministic Belirlenimci bir bütünleştirilmiş kod üretir (modül sürümü GUID'si ve zaman damgası dahil) --refonly Ana çıkış yerine bir başvuru derlemesi üretir +-refonly Ana çıktı yerine bir başvuru bütünleştirilmiş kodu üretir -instrument:TestCoverage Kapsam bilgilerini toplamak için işaretlenmiş - bir derleme üretir --sourcelink:<file> PDB dosyasına eklenecek kaynak bağlantısı bilgileri. + bir bütünleştirilmiş kod üretir +-sourcelink:<file> PDB’ye eklenecek kaynak bağlantısı bilgileri. - HATALAR VE UYARILAR - -nowarn Tüm uyarıları devre dışı bırakır. @@ -466,7 +466,7 @@ -errorlog:<file>[,version=<sarif_version>] Tüm derleyici ve çözümleyici tanılamalarının SARIF biçiminde günlüğe kaydedilmesi için bir dosya belirtir. - sarif_version:{1|2|2.1} Varsayılan olarak 1. 2 ve 2.1 + sarif_version:{1|2|2.1} Varsayılan olarak 1. 2 ve 2.1’dir Her ikisi de 2.1.0 SARIF sürümünü ifade eder. -reportanalyzer Yürütme zamanı gibi ek çözümleyici bilgilerini raporlar. @@ -477,23 +477,23 @@ bildirir. symbol_list:name=value,... (Kısa biçimi: -d) -imports:<import_list> Başvurulan meta veri dosyalarında ad alanları - için genel İçeri Aktarma işlemlerini bildirir. + için global İçeri Aktarma işlemlerini bildirir. import_list:namespace,... -langversion:? Dil sürümü için izin verilen değerleri görüntüler -langversion:<string> `default` (en son birincil sürüm) veya `latest` (ikincil sürümler dahil en son sürüm) veya `14` veya `15.3` gibi belirli sürümler gibi - dil sürümünü belirtir. + dil sürümünü belirtir -optionexplicit[+|-] Değişkenlerin açıkça bildirilmesini gerektirir. -optioninfer[+|-] Değişkenlerin tür çıkarımına izin verir. -rootnamespace:<string> Tüm tür bildirimleri için kök Ad Alanını belirtir. --optionstrict[+|-] Kesin dil semantik kurallarının uygulanmasını zorlar. --optionstrict:custom Kesin dil semantik kurallarına uyulmadığında +-optionstrict[+|-] Katı dil semantik kurallarının uygulanmasını zorlar. +-optionstrict:custom Katı dil semantik kurallarına uyulmadığında uyarır. --optioncompare:binary İkili stilinde dize karşılaştırmaları yapılacağını belirtir. +-optioncompare:binary İkili stilinde dize karşılaştırmalarını belirtir. Bu varsayılan değerdir. --optioncompare:text Metin stilinde dize karşılaştırmaları yapılacağını belirtir. +-optioncompare:text Metin stilinde dize karşılaştırmalarını belirtir. - DİĞER - -help Bu kullanım iletisini görüntüler. (Kısa biçimi: -?) @@ -507,32 +507,32 @@ - GELİŞMİŞ - -baseaddress:<number> Kitaplık veya modülün temel adresi (onaltılık). --checksumalgorithm:<alg> PDB dosyasında depolanan kaynak dosya - sağlama toplamını hesaplamak için kullanılan algoritmayı belirtir. Desteklenen değerler şunlardır: - SHA1 veya SHA256 (varsayılan). --codepage:<number> Kaynak dosyalar açılırken kullanılan kod sayfasını +-checksumalgorithm:<alg> PDB’de depolanan kaynak dosya + sağlama toplamını hesaplamaya yönelik algoritmayı belirtir. Desteklenen değerler şunlardır: + SHA1, SHA256 (varsayılan), SHA384 veya SHA512. +-codepage:<number> Kaynak dosyalar açılırken kullanılacak kod sayfasını belirtir. --delaysign[+|-] Tanımlayıcı ad anahtarının yalnızca ortak bölümünü - kullanarak derlemeyi gecikmeli imzalar. --publicsign[+|-] Tanımlayıcı ad anahtarının yalnızca ortak bölümünü - kullanarak derlemeyi genel imzalar. +-delaysign[+|-] Güçlü ad anahtarının yalnızca ortak bölümünü + kullanarak bütünleştirilmiş kodu gecikmeli imzalar. +-publicsign[+|-] Güçlü ad anahtarının yalnızca ortak bölümünü + kullanarak bütünleştirilmiş kodu genel imzalar. -errorreport:<string> İç derleyici hatalarının nasıl işleneceğini belirtir; prompt, send, none veya queue (varsayılan) olmalıdır. -generatedfilesout:<dir> Derleme sırasında oluşturulan dosyaları - belirtilen dizinde yerleştirir. --filealign:<number> Çıkış dosyası bölümleri için kullanılan hizalamayı + belirtilen dizine yerleştirir. +-filealign:<number> Çıktı dosyası bölümleri için kullanılan hizalamayı belirtir. -highentropyva[+|-] Yüksek entropili ASLR’yi etkinleştirir. --keycontainer:<string> Tanımlayıcı ad anahtarı kapsayıcısını belirtir. --keyfile:<file> Tanımlayıcı ad anahtarı dosyasını belirtir. +-keycontainer:<string> Güçlü ad anahtarı kapsayıcısını belirtir. +-keyfile:<file> Güçlü ad anahtarı dosyasını belirtir. -libpath:<path_list> Meta veri başvurularını aramak için kullanılacak dizinlerin listesi. (Noktalı virgülle ayrılmış.) -main:<class> Alt Ana dosyayı içeren Sınıfı veya Modülü belirtir. Bu ayrıca System.Windows.Forms.Form’dan devralan bir Sınıf da olabilir. (Kısa biçimi: -m) --moduleassemblyname:<string> Bu modülün bir parçası olacağı derlemenin +-moduleassemblyname:<string> Bu modülün bir parçası olacağı bütünleştirilmiş kodun adı. -netcf .NET Compact Framework'ü hedefler. -nostdlib Standart kitaplıklara (system.dll ve VBC.RSP dosyası) @@ -543,18 +543,18 @@ -platform:<string> Bu kodun çalışabileceği platformları sınırlar; x86, x64, Itanium, arm, arm64 AnyCPU32BitPreferred veya anycpu (varsayılan) olmalıdır. --preferreduilang Tercih edilen çıkış dili adını belirtir. +-preferreduilang Tercih edilen çıktı dili adını belirtir. -nosdkpath Standart kitaplık derlemeleri için varsayılan SDK yolunu aramayı devre dışı bırakır. --reportivts[+|-] Bu derlemeye tüm bağımlılıklar tarafından verilen tüm IVT'ler hakkında çıkış bilgisi - ve hangi derlemeden geldikleri dahil olmak üzere - yabancı derleme erişilebilirlik hataları hakkında ek açıklamalar. +-reportivts[+|-] Bu bütünleştirilmiş koda tüm bağımlılıklar tarafından verilen tüm IVT'ler hakkında bilgi çıktısı oluşturur + ve hangi bütünleştirilmiş koddan geldikleri dahil olmak üzere + yabancı bütünleştirilmiş kod erişilebilirlik hataları hakkında not ekler. -sdkpath:<path> Standart kitaplık derlemelerini aramak için kullanılan yol. --subsystemversion:<version> Çıkış PE’sinin alt sistem sürümünü belirtir. +-subsystemversion:<version> Çıktı PE’sinin alt sistem sürümünü belirtir. version:<number>[.<number>] --utf8output[+|-] Derleyici çıkışını UTF-8 karakter kodlamasıyla +-utf8output[+|-] Derleyici çıktısını UTF-8 karakter kodlamasıyla gösterir. -@<file> Metin dosyasındaki komut satırı ayarlarını ekler --vbruntime[+|-|*] Varsayılan Visual Basic çalışma zamanı ile ve +@<file> Metin dosyasından komut satırı ayarlarını ekler +-vbruntime[+|-|*] Varsayılan Visual Basic çalışma zamanı ile veya çalışma zamanı olmadan derler. -vbruntime:<file> <file> içindeki alternatif Visual Basic çalışma zamanı ile derler. diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hans.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hans.xlf index 8c40d1b664a1..aa1ddd2cef03 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hans.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hans.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -401,21 +401,22 @@ -target:library 创建库程序集。 -target:module 创建可添加到 程序集的模块。 --target:appcontainerexe 创建在 AppContainer 中运行的 - Windows 应用程序。 +-target:appcontainerexe 创建在 + 中运行的 Windows 应用程序 + AppContainer。 -target:winmdobj 创建 Windows 元数据中间文件 -doc[+|-] 生成 XML 文档文件。 -doc:<file> 将 XML 文档文件生成到 <file>。 --refout:<file> 引用要生成的程序集输出 +-refout:<file> 要生成的引用程序集输出 - 输入文件 - --addmodule:<file_list> 从指定模块引用元数据 --link:<file_list> 从指定的互操作程序集嵌入 +-addmodule:<file_list> 引用指定模块中的元数据 +-link:<file_list> 嵌入来自指定互操作程序集的 元数据。(缩写: -l) -recurse:<wildcard> 根据通配符规范,包括 当前目录和子目录中的 所有文件。 --reference:<file_list> 从指定的程序集引用 +-reference:<file_list> 引用来自指定程序集的 元数据。(缩写: -r) -analyzer:<file_list> 从此程序集中运行分析器 (缩写: -a) @@ -481,7 +482,7 @@ import_list:namespace,... -langversion:? 显示语言版本的允许值 -langversion:<string> 指定语言版本,例如 - `default` (最新主版本)或 + `default` (最新主要版本)或 `latest` (最新版本,包括次要版本), 或特定版本,例如 `14` 或 `15.3` -o-optionexplicit[+|-] 需要显式声明变量。 @@ -508,8 +509,8 @@ -baseaddress:<number> 库或模块的基址 (十六进制)。 -checksumalgorithm:<alg> 指定用于计算存储在 PDB 中的源文件 - 校验和的算法。支持的值为: - SHA1 或 SHA256 (默认值)。 + 校验和的算法。支持的值为: + SHA1、SHA256(默认)、SHA384 或 SHA512。 -codepage:<number> 指定打开源文件时要使用的 代码页。 -delaysign[+|-] 仅使用强名称密钥的公共部分 @@ -548,12 +549,12 @@ -reportivts[+|-] 输出所有依赖项授予此程序集的 所有 IVT 的相关信息,并在外部程序集可访问性错误中 注释它们来自哪个程序集。 --sdkpath:<路径> 用于搜索标准库程序集的路径。 +-sdkpath:<path> 用于搜索标准库程序集的路径。 -subsystemversion:<version> 指定输出 PE 的子系统版本。 version:<number>[.<number>] --utf8output[+|-] 以 UTF-8 字符编码发出编译器 +-utf8output[+|-] 以 UTF-8 字符编码格式发出编译器 输出。 -@<file> 从文本文件插入命令行设置 +@<file> 插入文本文件中的命令行设置 -vbruntime[+|-|*] 在使用/不使用默认 Visual Basic 运行时的情况下 进行编译。 -vbruntime:<file> 使用 <file> 中的备用 Visual Basic 运行时 diff --git a/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hant.xlf b/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hant.xlf index de55399b99b4..c84400643c16 100644 --- a/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hant.xlf +++ b/src/Compilers/VisualBasic/Portable/xlf/VBResources.zh-Hant.xlf @@ -341,7 +341,7 @@ (hex). -checksumalgorithm:<alg> Specify algorithm for calculating source file checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). + SHA1, SHA256 (default), SHA384, or SHA512. -codepage:<number> Specifies the codepage to use when opening source files. -delaysign[+|-] Delay-sign the assembly using only the public @@ -391,173 +391,174 @@ -vbruntime:<file> Compile with the alternate Visual Basic runtime in <file>. - Visual Basic Compiler Options + Visual Basic 編譯器選項 - OUTPUT FILE - --out:<file> Specifies the output file name. --target:exe Create a console application (default). - (Short form: -t) --target:winexe Create a Windows application. --target:library Create a library assembly. --target:module Create a module that can be added to an - assembly. --target:appcontainerexe Create a Windows application that runs in - AppContainer. --target:winmdobj Create a Windows Metadata intermediate file --doc[+|-] Generates XML documentation file. --doc:<file> Generates XML documentation file to <file>. --refout:<file> Reference assembly output to generate +-out:<file> 指定輸出檔案名稱。 +-target:exe 建立主控台應用程式 (預設)。 + (簡短形式: -t) +-target:winexe 建立 Windows 應用程式。 +-target:library 建立程式庫組件。 +-target:module 建立可新增到 + 組件的模組。 +-target:appcontainerexe 建立可在 AppContainer 中執行的 + Windows 應用程式。 +-target:winmdobj 建立 Windows 中繼資料中繼檔案 +-doc[+|-] 產生 XML 文件檔案。 +-doc:<file> 產生 XML 文件檔案至 <file>。 +-refout:<file> 要產生的參考組件輸出 - INPUT FILES - --addmodule:<file_list> Reference metadata from the specified modules --link:<file_list> Embed metadata from the specified interop - assembly. (Short form: -l) --recurse:<wildcard> Include all files in the current directory - and subdirectories according to the - wildcard specifications. --reference:<file_list> Reference metadata from the specified - assembly. (Short form: -r) --analyzer:<file_list> Run the analyzers from this assembly - (Short form: -a) --additionalfile:<file list> Additional files that don't directly affect code - generation but may be used by analyzers for producing - errors or warnings. +-addmodule:<file_list> 從指定的模組參考中繼資料 +-link:<file_list> 從指定的 Interop + 組件參考中繼資料。(簡短形式: -l) +-recurse:<wildcard> 根據 +萬用字元 + 規格 + 包含目前目錄和子目錄中的所有檔案。 +-reference:<file_list> 從指定的 + 組件參考中繼資料。(簡短形式: -r) +-analyzer:<file_list> 從此組件執行分析器 + (簡短形式: -a) +-additionalfile:<file list> 不會直接影響程式碼產生 + 但可由分析器用來產生錯誤或警告 + 的其他檔案。 - RESOURCES - --linkresource:<resinfo> Links the specified file as an external - assembly resource. +-linkresource:<resinfo> 連結指定的檔案做為外部 + 組件資源。 resinfo:<file>[,<name>[,public|private]] - (Short form: -linkres) --nowin32manifest The default manifest should not be embedded - in the manifest section of the output PE. --resource:<resinfo> Adds the specified file as an embedded - assembly resource. + (簡短形式: -linkres) +-nowin32manifest 預設資訊清單不應該內嵌在 + 輸出 PE 的資訊清單區段。 +-resource:<resinfo> 將指定的檔案新增為內嵌 + 組件資源。 resinfo:<file>[,<name>[,public|private]] - (Short form: -res) --win32icon:<file> Specifies a Win32 icon file (.ico) for the - default Win32 resources. --win32manifest:<file> The provided file is embedded in the manifest - section of the output PE. --win32resource:<file> Specifies a Win32 resource file (.res). + (簡短形式: -res) +-win32icon:<file> 為預設 Win32 資源指定 + Win32 圖示檔案 (.ico)。 +-win32manifest:<file> 提供的檔案內嵌在 + 輸出 PE 的資訊清單區段。 +-win32resource:<file> 指定 Win32 資源檔案 (.res)。 - CODE GENERATION - --optimize[+|-] Enable optimizations. --removeintchecks[+|-] Remove integer checks. Default off. --debug[+|-] Emit debugging information. --debug:full Emit full debugging information (default). --debug:pdbonly Emit full debugging information. --debug:portable Emit cross-platform debugging information. --debug:embedded Emit cross-platform debugging information into - the target .dll or .exe. --deterministic Produce a deterministic assembly - (including module version GUID and timestamp) --refonly Produce a reference assembly in place of the main output --instrument:TestCoverage Produce an assembly instrumented to collect - coverage information --sourcelink:<file> Source link info to embed into PDB. +-optimize[+|-] 啟用最佳化。 +-removeintchecks[+|-] 移除整數檢查。預設為關閉。 +-debug[+|-] 發出偵錯資訊。 +-debug:full 發出完整偵錯資訊 (預設)。 +-debug:pdbonly 發出完整偵錯資訊。 +-debug:portable 發出跨平台偵錯資訊。 +-debug:embedded 發出跨平台偵錯資訊至 + 目標 .dll 或 .exe。 +-deterministic 產生具決定性組件 + (包括模組版本 GUID 和時間戳記) +-refonly 產生參考組件,以取代主要輸出 +-instrument:TestCoverage 產生檢測要收集 + 涵蓋範圍資訊的組件 +-sourcelink:<file> 要內嵌至 PDB 的來源連結資訊。 - ERRORS AND WARNINGS - --nowarn Disable all warnings. --nowarn:<number_list> Disable a list of individual warnings. --warnaserror[+|-] Treat all warnings as errors. --warnaserror[+|-]:<number_list> Treat a list of warnings as errors. --ruleset:<file> Specify a ruleset file that disables specific - diagnostics. +-nowarn 停用所有警告。 +-nowarn:<number_list> 停用個別警告的清單。 +-warnaserror[+|-] 將所有警告視為錯誤。 +-warnaserror[+|-]:<number_list> 將警告清單視為錯誤。 +-ruleset:<file> 指定會停用特定診斷的 + 規則集檔案。 -errorlog:<file>[,version=<sarif_version>] - Specify a file to log all compiler and analyzer - diagnostics in SARIF format. - sarif_version:{1|2|2.1} Default is 1. 2 and 2.1 - both mean SARIF version 2.1.0. --reportanalyzer Report additional analyzer information, such as - execution time. --skipanalyzers[+|-] Skip execution of diagnostic analyzers. + 指定用來以 SARIF 格式記錄所有編譯器和分析器診斷 + 的檔案。 + sarif_version:{1|2|2.1} 預設為 1。2 和 2.1 + 都表示 SARIF 版本 2.1.0。 +-reportanalyzer 報告其他分析器資訊,例如 + 執行時間。 +-skipanalyzers[+|-] 略過診斷分析器執行。 - LANGUAGE - --define:<symbol_list> Declare global conditional compilation - symbol(s). symbol_list:name=value,... - (Short form: -d) --imports:<import_list> Declare global Imports for namespaces in - referenced metadata files. +-define:<symbol_list> 宣告全域條件式編譯 + 符號。symbol_list:name=value,... + (簡短形式: -d) +-imports:<import_list> 在參考的中繼資料檔案中 + 宣告命名空間的全域匯入。 import_list:namespace,... --langversion:? Display the allowed values for language version --langversion:<string> Specify language version such as - `default` (latest major version), or - `latest` (latest version, including minor versions), - or specific versions like `14` or `15.3` --optionexplicit[+|-] Require explicit declaration of variables. --optioninfer[+|-] Allow type inference of variables. --rootnamespace:<string> Specifies the root Namespace for all type - declarations. --optionstrict[+|-] Enforce strict language semantics. --optionstrict:custom Warn when strict language semantics are not - respected. --optioncompare:binary Specifies binary-style string comparisons. - This is the default. --optioncompare:text Specifies text-style string comparisons. +-langversion:? 顯示語言版本的允許值 +-langversion:<string> 指定語言版本,例如 + `default` (最新的主要版本),或 + `latest` (最新版本,包括次要版本), + 或特定版本,例如 `14` 或 `15.3` +-optionexplicit[+|-] 需要明確宣告變數。 +-optioninfer[+|-] 允許變數的類型推斷。 +-rootnamespace:<string> 指定所有類型宣告 + 的根命名空間。 +-optionstrict[+|-] 強制執行嚴格的語言語意。 +-optionstrict:custom 未遵守嚴格的語言語意時提出警告 + 。 +-optioncompare:binary 指定二進位樣式字串比較。 + 這是預設值。 +-optioncompare:text 指定文字樣式字串比較。 - MISCELLANEOUS - --help Display this usage message. (Short form: -?) --noconfig Do not auto-include VBC.RSP file. --nologo Do not display compiler copyright banner. --quiet Quiet output mode. --verbose Display verbose messages. --parallel[+|-] Concurrent build. --version Display the compiler version number and exit. +-help 顯示使用方式訊息。(簡短形式: -?) +-noconfig 不要自動包括 VBC.RSP 檔案。 +-nologo 不要顯示編譯器著作權橫幅。 +-quiet 無訊息輸出模式。 +-verbose 顯示詳細訊息。 +-parallel[+|-] 同時建置。 +-version 顯示編譯器版本號碼並結束。 - ADVANCED - --baseaddress:<number> The base address for a library or module - (hex). --checksumalgorithm:<alg> Specify algorithm for calculating source file - checksum stored in PDB. Supported values are: - SHA1 or SHA256 (default). --codepage:<number> Specifies the codepage to use when opening - source files. --delaysign[+|-] Delay-sign the assembly using only the public - portion of the strong name key. --publicsign[+|-] Public-sign the assembly using only the public - portion of the strong name key. --errorreport:<string> Specifies how to handle internal compiler - errors; must be prompt, send, none, or queue - (default). --generatedfilesout:<dir> Place files generated during compilation in the - specified directory. --filealign:<number> Specify the alignment used for output file - sections. --highentropyva[+|-] Enable high-entropy ASLR. --keycontainer:<string> Specifies a strong name key container. --keyfile:<file> Specifies a strong name key file. --libpath:<path_list> List of directories to search for metadata - references. (Semi-colon delimited.) --main:<class> Specifies the Class or Module that contains - Sub Main. It can also be a Class that - inherits from System.Windows.Forms.Form. - (Short form: -m) --moduleassemblyname:<string> Name of the assembly which this module will - be a part of. --netcf Target the .NET Compact Framework. --nostdlib Do not reference standard libraries - (system.dll and VBC.RSP file). +-baseaddress:<number> 程式庫或模組的基本位址 + (十六進位)。 +-checksumalgorithm:<alg> 指定計算儲存在 PDB 中 + 來源檔案總和檢查碼的演算法。支援的值為: + SHA1、SHA256 (預設)、SHA384 或 SHA512。 +-codepage:<number> 指定開啟來源檔案時 + 要使用的字碼頁。 +-delaysign[+|-] 只使用強式名稱金鑰的公開 + 部分對組件進行延遲簽屬。 +-publicsign[+|-] 只使用強式名稱金鑰的公開 + 部分對組件進行公開簽屬。 +-errorreport:<string> 指定如何處理內部編譯器 + 錯誤; 必須是提示、傳送、無或佇列 + (預設)。 +-generatedfilesout:<dir> 將編譯期間產生的檔案放在 + 指定的目錄。 +-filealign:<number> 指定用於輸出檔案 + 區段的對齊。 +-highentropyva[+|-] 啟用高熵 ASLR。 +-keycontainer:<string> 指定強式名稱金鑰容器。 +-keyfile:<file> 指定強式名稱金鑰檔案。 +-libpath:<path_list> 用於搜尋中繼資料 + 參考的目錄清單。(以分號分隔。) +-main:<class> 指定包含 + Sub Main 的類別或模組。它也可以是 + 繼承自 System.Windows.Forms.Form 的類別。 + (簡短形式: -m) +-moduleassemblyname:<string> 此模組將成為 + 其一部分的組件名稱。 +-netcf 以 .NET Compact Framework 為目標。 +-nostdlib 不參考標準程式庫 + (system.dll 和 VBC.RSP 檔案)。 -pathmap:<K1>=<V1>,<K2>=<V2>,... - Specify a mapping for source path names output by - the compiler. --platform:<string> Limit which platforms this code can run on; - must be x86, x64, Itanium, arm, arm64 - AnyCPU32BitPreferred or anycpu (default). --preferreduilang Specify the preferred output language name. --nosdkpath Disable searching the default SDK path for standard library assemblies. --reportivts[+|-] Output information on all IVTs granted to this - assembly by all dependencies, and annotate foreign assembly - accessibility errors with what assembly they came from. --sdkpath:<path> Path used to search for standard library assemblies. --subsystemversion:<version> Specify subsystem version of the output PE. + 指定由編譯器輸出之來源路徑名稱的對應 + 。 +-platform:<string> 限制此程式碼可在哪些平台上執行; + 必須是 x86、x64、Itanium、arm、arm64 + AnyCPU32BitPreferred 或 anycpu (預設)。 +-preferreduilang 指定喜好的輸出語言名稱。 +-nosdkpath 停用搜尋標準程式庫組件的預設 SDK 路徑。 +-reportivts[+|-] 輸出有關所有相依項授與至此 + 組件的所有 IVT 的資訊,並使用它們來自的組件 + 標註外部組件的可存取性錯誤。 +-sdkpath:<path> 用來搜尋標準文件庫組件的路徑。 +-subsystemversion:<version> 指定輸出 PE 的子系統版本。 version:<number>[.<number>] --utf8output[+|-] Emit compiler output in UTF-8 character - encoding. -@<file> Insert command-line settings from a text file --vbruntime[+|-|*] Compile with/without the default Visual Basic - runtime. --vbruntime:<file> Compile with the alternate Visual Basic - runtime in <file>. +-utf8output[+|-] 以 UTF-8 字元編碼 + 發出編譯器輸出。 +@<file> 從文字檔插入命令列設定 +-vbruntime[+|-|*] 使用/不使用預設 Visual Basic + 執行階段編譯。 +-vbruntime:<file> 使用 <file> 中的其他 Visual Basic + 執行階段編譯。 diff --git a/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb b/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb index 117b27e32069..243fc0f941e5 100644 --- a/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb +++ b/src/Compilers/VisualBasic/Test/CommandLine/CommandLineTests.vb @@ -1904,6 +1904,16 @@ End Module").Path Assert.Equal(SourceHashAlgorithm.Sha256, parsedArgs.ChecksumAlgorithm) Assert.Equal(HashAlgorithmName.SHA256, parsedArgs.EmitOptions.PdbChecksumAlgorithm) + parsedArgs = DefaultParse({"/checksumAlgorithm:sHa384", "a.cs"}, _baseDirectory) + parsedArgs.Errors.Verify() + Assert.Equal(SourceHashAlgorithm.Sha384, parsedArgs.ChecksumAlgorithm) + Assert.Equal(HashAlgorithmName.SHA256, parsedArgs.EmitOptions.PdbChecksumAlgorithm) + + parsedArgs = DefaultParse({"/checksumAlgorithm:sha512", "a.cs"}, _baseDirectory) + parsedArgs.Errors.Verify() + Assert.Equal(SourceHashAlgorithm.Sha512, parsedArgs.ChecksumAlgorithm) + Assert.Equal(HashAlgorithmName.SHA256, parsedArgs.EmitOptions.PdbChecksumAlgorithm) + parsedArgs = DefaultParse({"a.cs"}, _baseDirectory) parsedArgs.Errors.Verify() Assert.Equal(SourceHashAlgorithm.Sha256, parsedArgs.ChecksumAlgorithm) @@ -8947,7 +8957,7 @@ End Class Nothing, _baseDirectory, {"/reportanalyzer", "/t:library", source}, - analyzers:={New WarningDiagnosticAnalyzer()}, + analyzers:={New WarningDiagnosticAnalyzer(), New ConcurrentAnalyzer({"C"}), New DiagnosticSuppressorForId("Warning01", "Suppressor01")}, generators:={New DoNothingGenerator().AsSourceGenerator()}) Dim outWriter = New StringWriter() Dim exitCode = vbc.Run(outWriter, Nothing) @@ -8955,8 +8965,44 @@ End Class Dim output = outWriter.ToString() Assert.Contains(New WarningDiagnosticAnalyzer().ToString(), output, StringComparison.Ordinal) Assert.Contains(CodeAnalysisResources.AnalyzerExecutionTimeColumnHeader, output, StringComparison.Ordinal) + Assert.Contains($"{NameOf(DiagnosticSuppressorForId)} (Suppressor01)", output, StringComparison.Ordinal) Assert.Contains(CodeAnalysisResources.GeneratorNameColumnHeader, output, StringComparison.Ordinal) Assert.Contains(GetType(DoNothingGenerator).FullName, output, StringComparison.Ordinal) + + Assert.DoesNotContain(CodeAnalysisResources.AllAnalyzersConcurrentMessage, output, StringComparison.Ordinal) + Dim nonConcurrentSection = output.Substring(output.IndexOf(CodeAnalysisResources.NonConcurrentAnalyzersHeader)) + Assert.Contains(NameOf(WarningDiagnosticAnalyzer), nonConcurrentSection, StringComparison.Ordinal) + Assert.DoesNotContain(NameOf(DiagnosticSuppressorForId), nonConcurrentSection, StringComparison.Ordinal) + Assert.DoesNotContain(GetType(ConcurrentAnalyzer).Assembly.FullName, nonConcurrentSection, StringComparison.Ordinal) + Assert.DoesNotContain(NameOf(ConcurrentAnalyzer), nonConcurrentSection, StringComparison.Ordinal) + CleanupAllGeneratedFiles(source) + End Sub + + + Public Sub ReportAnalyzerOutput_AllConcurrentAnalyzers() + Dim source As String = Temp.CreateFile().WriteAllText( +Class C +End Class +.Value).Path + + Dim vbc = New MockVisualBasicCompiler( + Nothing, + _baseDirectory, + {"/reportanalyzer", "/t:library", source}, + analyzers:={New ConcurrentAnalyzer({"C"}), New DiagnosticSuppressorForId("Warning01", "Suppressor01")}, + generators:={New DoNothingGenerator().AsSourceGenerator()}) + Dim outWriter = New StringWriter() + Dim exitCode = vbc.Run(outWriter, Nothing) + Assert.Equal(0, exitCode) + Dim output = outWriter.ToString() + Assert.Contains(CodeAnalysisResources.AnalyzerExecutionTimeColumnHeader, output, StringComparison.Ordinal) + Assert.DoesNotContain(New WarningDiagnosticAnalyzer().ToString(), output, StringComparison.Ordinal) + Assert.Contains($"{NameOf(DiagnosticSuppressorForId)} (Suppressor01)", output, StringComparison.Ordinal) + Assert.Contains(CodeAnalysisResources.GeneratorNameColumnHeader, output, StringComparison.Ordinal) + Assert.Contains(GetType(DoNothingGenerator).FullName, output, StringComparison.Ordinal) + + Assert.DoesNotContain(CodeAnalysisResources.NonConcurrentAnalyzersHeader, output, StringComparison.Ordinal) + Assert.Contains(CodeAnalysisResources.AllAnalyzersConcurrentMessage, output, StringComparison.Ordinal) CleanupAllGeneratedFiles(source) End Sub diff --git a/src/Compilers/VisualBasic/Test/Emit/CodeGen/CodeGenClosureLambdaTests.vb b/src/Compilers/VisualBasic/Test/Emit/CodeGen/CodeGenClosureLambdaTests.vb index fe6b141bef61..84b08a96b714 100644 --- a/src/Compilers/VisualBasic/Test/Emit/CodeGen/CodeGenClosureLambdaTests.vb +++ b/src/Compilers/VisualBasic/Test/Emit/CodeGen/CodeGenClosureLambdaTests.vb @@ -1015,7 +1015,13 @@ label2: Public Sub Main() Dim x as C1 = New C1 - x.Goo(3) + Try + x.Goo(3) + Catch ex As Exception + Console.WriteLine(ex) + Console.WriteLine(ex.Message) + Console.WriteLine(ex.StackTrace) + End Try End Sub End Module diff --git a/src/Compilers/VisualBasic/Test/Emit/Emit/EditAndContinue/EditAndContinueClosureTests.vb b/src/Compilers/VisualBasic/Test/Emit/Emit/EditAndContinue/EditAndContinueClosureTests.vb index a364f410ad73..9a63c3e45272 100644 --- a/src/Compilers/VisualBasic/Test/Emit/Emit/EditAndContinue/EditAndContinueClosureTests.vb +++ b/src/Compilers/VisualBasic/Test/Emit/Emit/EditAndContinue/EditAndContinueClosureTests.vb @@ -2667,7 +2667,7 @@ _Lambda$__0 End Using End Sub - + Public Sub CeaseCapture_LastLocal() Using test = New EditAndContinueTest() test.AddBaseline( diff --git a/src/Compilers/VisualBasic/Test/Emit/Emit/EditAndContinue/EditAndContinueTests.vb b/src/Compilers/VisualBasic/Test/Emit/Emit/EditAndContinue/EditAndContinueTests.vb index fffa92970b86..9a94b2143df5 100644 --- a/src/Compilers/VisualBasic/Test/Emit/Emit/EditAndContinue/EditAndContinueTests.vb +++ b/src/Compilers/VisualBasic/Test/Emit/Emit/EditAndContinue/EditAndContinueTests.vb @@ -1575,7 +1575,7 @@ raise_E End Using End Sub - + Public Sub Event_TypeChange() Using New EditAndContinueTest(). AddBaseline( @@ -2260,7 +2260,7 @@ End Module End Using End Sub - + Public Sub Property_TypeChange() Dim common = " @@ -6217,7 +6217,7 @@ _Lambda$__1-1 End Using End Sub - + Public Sub Method_Delete_WithLambda() Using test = New EditAndContinueTest() test.AddBaseline( @@ -6450,7 +6450,7 @@ _Lambda$__1#2-0#2 End Using End Sub - + Public Sub Method_Delete_WithLambda_AddedMethod() Using test = New EditAndContinueTest() test.AddBaseline( @@ -6567,7 +6567,7 @@ _Lambda$__1#1-0#1 End Using End Sub - + Public Sub Method_Delete_WithLambda_MultipleGenerations() Dim common = " Imports System diff --git a/src/Compilers/VisualBasic/Test/Symbol/SymbolsTests/Source/PropertyTests.vb b/src/Compilers/VisualBasic/Test/Symbol/SymbolsTests/Source/PropertyTests.vb index c96aaa1b5876..2982e2c8bc8b 100644 --- a/src/Compilers/VisualBasic/Test/Symbol/SymbolsTests/Source/PropertyTests.vb +++ b/src/Compilers/VisualBasic/Test/Symbol/SymbolsTests/Source/PropertyTests.vb @@ -8279,6 +8279,53 @@ BC32066: Type arguments are not valid because attributes cannot be generic. ]]>) End Sub + + + Public Sub PropertyIsReadOnly_WithTypeArgumentFromUnrelatedAssembly() + Dim source1 = + + + + + + Dim comp1 = CreateCompilation(source1) + comp1.AssertNoDiagnostics() + + Dim source2 = + + + + + + Dim comp2 = CreateCompilation(source2, references:={comp1.ToMetadataReference()}) + comp2.AssertNoDiagnostics() + + ' Get the property symbol and check IsReadOnly + Dim c3 = comp2.GlobalNamespace.GetTypeMember("C3") + Dim [property] = c3.BaseTypeNoUseSiteDiagnostics.GetMember(Of PropertySymbol)("P") + + ' These should not throw an assertion + Assert.False([property].IsReadOnly) + Assert.False([property].IsWriteOnly) + End Sub + #Region "Helpers" Private Sub VerifyMethodsAndAccessorsSame(type As NamedTypeSymbol, [property] As PropertySymbol) VerifyMethodAndAccessorSame(type, [property], [property].GetMethod) diff --git a/src/Compilers/VisualBasic/Test/Symbol/SymbolsTests/WellKnownTypeValidationTests.vb b/src/Compilers/VisualBasic/Test/Symbol/SymbolsTests/WellKnownTypeValidationTests.vb index f7c966f668a4..5ccb3bf0a217 100644 --- a/src/Compilers/VisualBasic/Test/Symbol/SymbolsTests/WellKnownTypeValidationTests.vb +++ b/src/Compilers/VisualBasic/Test/Symbol/SymbolsTests/WellKnownTypeValidationTests.vb @@ -1,4 +1,4 @@ -' Licensed to the .NET Foundation under one or more agreements. +' Licensed to the .NET Foundation under one or more agreements. ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. @@ -539,6 +539,8 @@ End Namespace WellKnownType.System_Runtime_CompilerServices_NullablePublicOnlyAttribute, WellKnownType.System_Span_T, WellKnownType.System_ReadOnlySpan_T, + WellKnownType.System_Memory_T, + WellKnownType.System_ReadOnlyMemory_T, WellKnownType.System_Collections_Immutable_ImmutableArray_T, WellKnownType.System_Index, WellKnownType.System_Range, @@ -567,6 +569,8 @@ End Namespace WellKnownType.System_Runtime_CompilerServices_CompilerFeatureRequiredAttribute, WellKnownType.System_Runtime_CompilerServices_ScopedRefAttribute, WellKnownType.System_Runtime_CompilerServices_RefSafetyRulesAttribute, + WellKnownType.System_Runtime_CompilerServices_MemorySafetyRulesAttribute, + WellKnownType.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute, WellKnownType.System_Diagnostics_CodeAnalysis_UnscopedRefAttribute, WellKnownType.System_MemoryExtensions, WellKnownType.System_Runtime_CompilerServices_MetadataUpdateOriginalTypeAttribute, @@ -603,6 +607,8 @@ End Namespace WellKnownType.System_Runtime_CompilerServices_IsReadOnlyAttribute, WellKnownType.System_Runtime_CompilerServices_IsByRefLikeAttribute, WellKnownType.System_Runtime_CompilerServices_IsUnmanagedAttribute, + WellKnownType.System_Runtime_CompilerServices_UnionAttribute, + WellKnownType.System_Runtime_CompilerServices_IUnion, WellKnownType.System_Runtime_CompilerServices_ITuple, WellKnownType.System_Runtime_CompilerServices_HotReloadException, WellKnownType.System_Runtime_CompilerServices_MetadataUpdateDeletedAttribute @@ -641,6 +647,8 @@ End Namespace WellKnownType.System_Runtime_CompilerServices_NullablePublicOnlyAttribute, WellKnownType.System_Span_T, WellKnownType.System_ReadOnlySpan_T, + WellKnownType.System_Memory_T, + WellKnownType.System_ReadOnlyMemory_T, WellKnownType.System_Collections_Immutable_ImmutableArray_T, WellKnownType.System_Index, WellKnownType.System_Range, @@ -669,6 +677,8 @@ End Namespace WellKnownType.System_Runtime_CompilerServices_CompilerFeatureRequiredAttribute, WellKnownType.System_Runtime_CompilerServices_ScopedRefAttribute, WellKnownType.System_Runtime_CompilerServices_RefSafetyRulesAttribute, + WellKnownType.System_Runtime_CompilerServices_MemorySafetyRulesAttribute, + WellKnownType.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute, WellKnownType.System_Diagnostics_CodeAnalysis_UnscopedRefAttribute, WellKnownType.System_Runtime_CompilerServices_MetadataUpdateOriginalTypeAttribute, WellKnownType.System_Runtime_InteropServices_MemoryMarshal, @@ -704,6 +714,8 @@ End Namespace WellKnownType.System_Runtime_CompilerServices_IsReadOnlyAttribute, WellKnownType.System_Runtime_CompilerServices_IsByRefLikeAttribute, WellKnownType.System_Runtime_CompilerServices_IsUnmanagedAttribute, + WellKnownType.System_Runtime_CompilerServices_UnionAttribute, + WellKnownType.System_Runtime_CompilerServices_IUnion, WellKnownType.System_Runtime_CompilerServices_ITuple, WellKnownType.System_Runtime_CompilerServices_HotReloadException, WellKnownType.System_Runtime_CompilerServices_MetadataUpdateDeletedAttribute @@ -750,12 +762,14 @@ End Namespace WellKnownMember.System_Span_T__get_Item, WellKnownMember.System_Span_T__get_Length, WellKnownMember.System_Span_T__Slice_Int_Int, + WellKnownMember.System_Span_T__Slice_Int, WellKnownMember.System_ReadOnlySpan_T__ctor_Pointer, WellKnownMember.System_ReadOnlySpan_T__ctor_Array, WellKnownMember.System_ReadOnlySpan_T__ctor_Array_Start_Length, WellKnownMember.System_ReadOnlySpan_T__get_Item, WellKnownMember.System_ReadOnlySpan_T__get_Length, WellKnownMember.System_ReadOnlySpan_T__Slice_Int_Int, + WellKnownMember.System_ReadOnlySpan_T__Slice_Int, WellKnownMember.System_Runtime_CompilerServices_AsyncIteratorStateMachineAttribute__ctor, WellKnownMember.System_IAsyncDisposable__DisposeAsync, WellKnownMember.System_Collections_Generic_IAsyncEnumerable_T__GetAsyncEnumerator, @@ -791,6 +805,8 @@ End Namespace WellKnownMember.System_Diagnostics_CodeAnalysis_SetsRequiredMembersAttribute__ctor, WellKnownMember.System_Runtime_CompilerServices_ScopedRefAttribute__ctor, WellKnownMember.System_Runtime_CompilerServices_RefSafetyRulesAttribute__ctor, + WellKnownMember.System_Runtime_CompilerServices_MemorySafetyRulesAttribute__ctor, + WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor, WellKnownMember.System_MemoryExtensions__SequenceEqual_Span_T, WellKnownMember.System_MemoryExtensions__SequenceEqual_ReadOnlySpan_T, WellKnownMember.System_MemoryExtensions__AsSpan_String, @@ -842,6 +858,7 @@ End Namespace WellKnownMember.System_Runtime_CompilerServices_IsReadOnlyAttribute__ctor, WellKnownMember.System_Runtime_CompilerServices_IsByRefLikeAttribute__ctor, WellKnownMember.System_Runtime_CompilerServices_IsUnmanagedAttribute__ctor, + WellKnownMember.System_Runtime_CompilerServices_UnionAttribute__ctor, WellKnownMember.System_Index__ctor, WellKnownMember.System_Index__GetOffset, WellKnownMember.System_Range__ctor, @@ -874,7 +891,11 @@ End Namespace WellKnownMember.System_Runtime_CompilerServices_HotReloadException__ctorStringInt32, WellKnownMember.System_Runtime_CompilerServices_MetadataUpdateDeletedAttribute__ctor, WellKnownMember.System_Text_Encoding__get_UTF8, - WellKnownMember.System_Text_Encoding__GetString + WellKnownMember.System_Text_Encoding__GetString, + WellKnownMember.System_Memory_T__Slice_Int, + WellKnownMember.System_Memory_T__Slice_Int_Int, + WellKnownMember.System_ReadOnlyMemory_T__Slice_Int, + WellKnownMember.System_ReadOnlyMemory_T__Slice_Int_Int ' Not always available. Continue For End Select @@ -963,12 +984,14 @@ End Namespace WellKnownMember.System_Span_T__ctor_Array, WellKnownMember.System_Span_T__get_Item, WellKnownMember.System_Span_T__get_Length, + WellKnownMember.System_Span_T__Slice_Int, WellKnownMember.System_Span_T__Slice_Int_Int, WellKnownMember.System_ReadOnlySpan_T__ctor_Pointer, WellKnownMember.System_ReadOnlySpan_T__ctor_Array, WellKnownMember.System_ReadOnlySpan_T__ctor_Array_Start_Length, WellKnownMember.System_ReadOnlySpan_T__get_Item, WellKnownMember.System_ReadOnlySpan_T__get_Length, + WellKnownMember.System_ReadOnlySpan_T__Slice_Int, WellKnownMember.System_ReadOnlySpan_T__Slice_Int_Int, WellKnownMember.System_Runtime_CompilerServices_AsyncIteratorStateMachineAttribute__ctor, WellKnownMember.System_IAsyncDisposable__DisposeAsync, @@ -1004,6 +1027,8 @@ End Namespace WellKnownMember.System_Runtime_CompilerServices_RequiredMemberAttribute__ctor, WellKnownMember.System_Diagnostics_CodeAnalysis_SetsRequiredMembersAttribute__ctor, WellKnownMember.System_Runtime_CompilerServices_RefSafetyRulesAttribute__ctor, + WellKnownMember.System_Runtime_CompilerServices_MemorySafetyRulesAttribute__ctor, + WellKnownMember.System_Diagnostics_CodeAnalysis_RequiresUnsafeAttribute__ctor, WellKnownMember.System_Runtime_CompilerServices_ScopedRefAttribute__ctor, WellKnownMember.System_MemoryExtensions__SequenceEqual_Span_T, WellKnownMember.System_MemoryExtensions__SequenceEqual_ReadOnlySpan_T, @@ -1065,6 +1090,7 @@ End Namespace WellKnownMember.System_Range__get_End, WellKnownMember.System_Range__get_Start, WellKnownMember.System_Runtime_CompilerServices_IsUnmanagedAttribute__ctor, + WellKnownMember.System_Runtime_CompilerServices_UnionAttribute__ctor, WellKnownMember.System_Runtime_CompilerServices_ITuple__get_Item, WellKnownMember.System_Runtime_CompilerServices_ITuple__get_Length, WellKnownMember.System_Runtime_CompilerServices_SwitchExpressionException__ctor, @@ -1088,7 +1114,11 @@ End Namespace WellKnownMember.System_Runtime_CompilerServices_HotReloadException__ctorStringInt32, WellKnownMember.System_Runtime_CompilerServices_MetadataUpdateDeletedAttribute__ctor, WellKnownMember.System_Text_Encoding__get_UTF8, - WellKnownMember.System_Text_Encoding__GetString + WellKnownMember.System_Text_Encoding__GetString, + WellKnownMember.System_Memory_T__Slice_Int, + WellKnownMember.System_Memory_T__Slice_Int_Int, + WellKnownMember.System_ReadOnlyMemory_T__Slice_Int, + WellKnownMember.System_ReadOnlyMemory_T__Slice_Int_Int ' Not always available. Continue For End Select diff --git a/src/Compilers/VisualBasic/vbc/AnyCpu/vbc.csproj b/src/Compilers/VisualBasic/vbc/AnyCpu/vbc.csproj index 7a2f3558bbf1..254d23b9d6af 100644 --- a/src/Compilers/VisualBasic/vbc/AnyCpu/vbc.csproj +++ b/src/Compilers/VisualBasic/vbc/AnyCpu/vbc.csproj @@ -5,7 +5,6 @@ Exe $(NetRoslynSourceBuild);net472 true - false diff --git a/src/Dependencies/PooledObjects/ArrayBuilder.cs b/src/Dependencies/PooledObjects/ArrayBuilder.cs index 2c6e52b5bc25..b5aa35e87194 100644 --- a/src/Dependencies/PooledObjects/ArrayBuilder.cs +++ b/src/Dependencies/PooledObjects/ArrayBuilder.cs @@ -633,6 +633,14 @@ public void AddRange(ImmutableArray items) _builder.AddRange(items); } + public void AddRange(ImmutableArray items, Func selector) + { + foreach (var item in items) + { + _builder.Add(selector(item)); + } + } + public void AddRange(ImmutableArray items, int length) { _builder.AddRange(items, length); diff --git a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs index fdf6ae572836..1b18783a1d83 100644 --- a/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs +++ b/src/EditorFeatures/CSharp/StringCopyPaste/StringCopyPasteHelpers.cs @@ -416,7 +416,7 @@ private static string EscapeForNonRawVerbatimStringLiteral(bool isInterpolated, // First, go through and see if we're escaping *anything* in the original. If so, then we'll escape // everything. In other words, say we're pasting `[SuppressMessage("", "CA2013")]`. We technically don't // need to escape the `""` (since that is legal in a verbatim string). However, we will be escaping the - // quotes in teh `"CA2013"` to become `""CA2013""`. Once we decide we're escaping some quotes, we should + // quotes in the `"CA2013"` to become `""CA2013""`. Once we decide we're escaping some quotes, we should // then realize that we *should* escape the `""` to `""""` to be consistent. // So if we determine that we will be escaping all code, then just recurse, this time setting diff --git a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs index 30da5d04139e..ee4c72bdcbb9 100644 --- a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs +++ b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests.cs @@ -1429,77 +1429,6 @@ public async Task ShebangNotAsFirstCommentInScript(TestHost testHost) await TestAsync(code, code, testHost, Options.Script, expected); } - [Theory, CombinatorialData] - public Task IgnoredDirective_01(TestHost testHost) - => TestAsync(""" - #:unknown // comment - Console.Write(); - """, - testHost, - PPKeyword("#"), - PPKeyword(":"), - PPKeyword("unknown"), - String(" // comment"), - Identifier("Console"), - Operators.Dot, - Identifier("Write"), - Punctuation.OpenParen, - Punctuation.CloseParen, - Punctuation.Semicolon); - - [Theory, CombinatorialData] - public Task IgnoredDirective_02(TestHost testHost) - => TestAsync(""" - #:sdk Test 2.1.0 - Console.Write(); - """, - testHost, - PPKeyword("#"), - PPKeyword(":"), - PPKeyword("sdk"), - String(" Test 2.1.0"), - Identifier("Console"), - Operators.Dot, - Identifier("Write"), - Punctuation.OpenParen, - Punctuation.CloseParen, - Punctuation.Semicolon); - - [Theory, CombinatorialData] - public Task IgnoredDirective_03(TestHost testHost) - => TestAsync(""" - #:no-space - Console.Write(); - """, - testHost, - PPKeyword("#"), - PPKeyword(":"), - PPKeyword("no-space"), - Identifier("Console"), - Operators.Dot, - Identifier("Write"), - Punctuation.OpenParen, - Punctuation.CloseParen, - Punctuation.Semicolon); - - [Theory, CombinatorialData] - public Task IgnoredDirective_04(TestHost testHost) - => TestAsync($""" - #:sdk{'\t'}Test 2.1.0 - Console.Write(); - """, - testHost, - PPKeyword("#"), - PPKeyword(":"), - PPKeyword("sdk"), - String("\tTest 2.1.0"), - Identifier("Console"), - Operators.Dot, - Identifier("Write"), - Punctuation.OpenParen, - Punctuation.CloseParen, - Punctuation.Semicolon); - [Theory, CombinatorialData] public Task CommentAsMethodBodyContent(TestHost testHost) => TestAsync(""" @@ -2159,6 +2088,21 @@ public Task StructTypeDeclaration1(TestHost testHost) Punctuation.OpenCurly, Punctuation.CloseCurly); + [Theory, CombinatorialData] + public Task UnionTypeDeclaration1(TestHost testHost) + => TestAsync("union Union1(int, Union1) { }", + testHost, + TestOptions.RegularNext, + Keyword("union"), + Struct("Union1"), + Punctuation.OpenParen, + Keyword("int"), + Punctuation.Comma, + Identifier("Union1"), + Punctuation.CloseParen, + Punctuation.OpenCurly, + Punctuation.CloseCurly); + [Theory, CombinatorialData] public Task InterfaceDeclaration1(TestHost testHost) => TestAsync("interface I1 { }", diff --git a/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests_FileBasedApps.cs b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests_FileBasedApps.cs new file mode 100644 index 000000000000..e9e6c149c811 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/Classification/SyntacticClassifierTests_FileBasedApps.cs @@ -0,0 +1,399 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Remote.Testing; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; +using static Microsoft.CodeAnalysis.Editor.UnitTests.Classification.FormattedClassifications; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Classification; + +[Trait(Traits.Feature, Traits.Features.Classification)] +public partial class SyntacticClassifierTests +{ + [Theory, CombinatorialData] + public Task FileBasedApps_Sdk_01(TestHost testHost) + => TestAsync(""" + #:sdk Microsoft.Net.SDK + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk"), + Identifier("Microsoft"), + Punctuation.Text("."), + Identifier("Net"), + Punctuation.Text("."), + Identifier("SDK")); + + [Theory, CombinatorialData] + public Task FileBasedApps_Sdk_02(TestHost testHost) + => TestAsync(""" + #:sdk Microsoft.NET.Sdk.Web@10.0.100-preview.3 + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk"), + Identifier("Microsoft"), + Punctuation.Text("."), + Identifier("NET"), + Punctuation.Text("."), + Identifier("Sdk"), + Punctuation.Text("."), + Identifier("Web"), + Punctuation.Text("@"), + String("10.0.100-preview.3")); + + // sdk: @ separator with no version value after it + [Theory, CombinatorialData] + public Task FileBasedApps_Sdk_03(TestHost testHost) + => TestAsync(""" + #:sdk Microsoft.NET.Sdk@ + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk"), + Identifier("Microsoft"), + Punctuation.Text("."), + Identifier("NET"), + Punctuation.Text("."), + Identifier("Sdk"), + Punctuation.Text("@")); + + // sdk: duplicate @ separators + [Theory, CombinatorialData] + public Task FileBasedApps_Sdk_04(TestHost testHost) + => TestAsync(""" + #:sdk Microsoft.NET.Sdk@1.0@extra + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk"), + Identifier("Microsoft"), + Punctuation.Text("."), + Identifier("NET"), + Punctuation.Text("."), + Identifier("Sdk"), + Punctuation.Text("@"), + String("1.0@extra")); + + // sdk: no name, just @ and version + [Theory, CombinatorialData] + public Task FileBasedApps_Sdk_05(TestHost testHost) + => TestAsync(""" + #:sdk @1.0 + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk"), + Punctuation.Text("@"), + String("1.0")); + + [Theory, CombinatorialData] + public Task FileBasedApps_Sdk_06(TestHost testHost) + => TestAsync(""" + #:sdk Test 2.1.0 + Console.Write(); + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk"), + Identifier("Test 2"), + Punctuation.Text("."), + Identifier("1"), + Punctuation.Text("."), + Identifier("0"), + Identifier("Console"), + Operators.Dot, + Identifier("Write"), + Punctuation.OpenParen, + Punctuation.CloseParen, + Punctuation.Semicolon); + + [Theory, CombinatorialData] + public Task FileBasedApps_Sdk_07(TestHost testHost) + => TestAsync($""" + #:sdk{'\t'}Test 2.1.0 + Console.Write(); + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk"), + Identifier("Test 2"), + Punctuation.Text("."), + Identifier("1"), + Punctuation.Text("."), + Identifier("0"), + Identifier("Console"), + Operators.Dot, + Identifier("Write"), + Punctuation.OpenParen, + Punctuation.CloseParen, + Punctuation.Semicolon); + + [Theory, CombinatorialData] + public Task FileBasedApps_Package_01(TestHost testHost) + => TestAsync(""" + #:package Newtonsoft.Json@13.0.3 + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("package"), + Identifier("Newtonsoft"), + Punctuation.Text("."), + Identifier("Json"), + Punctuation.Text("@"), + String("13.0.3")); + + // package: @ separator with no version + [Theory, CombinatorialData] + public Task FileBasedApps_Package_02(TestHost testHost) + => TestAsync(""" + #:package Newtonsoft.Json@ + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("package"), + Identifier("Newtonsoft"), + Punctuation.Text("."), + Identifier("Json"), + Punctuation.Text("@")); + + // package: no @ separator, name only + [Theory, CombinatorialData] + public Task FileBasedApps_Package_03(TestHost testHost) + => TestAsync(""" + #:package Newtonsoft.Json + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("package"), + Identifier("Newtonsoft"), + Punctuation.Text("."), + Identifier("Json")); + + // package: duplicate @ separators + [Theory, CombinatorialData] + public Task FileBasedApps_Package_04(TestHost testHost) + => TestAsync(""" + #:package Pkg@1.0@extra + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("package"), + Identifier("Pkg"), + Punctuation.Text("@"), + String("1.0@extra")); + + // package: no name, just @ and version + [Theory, CombinatorialData] + public Task FileBasedApps_Package_05(TestHost testHost) + => TestAsync(""" + #:package @13.0.3 + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("package"), + Punctuation.Text("@"), + String("13.0.3")); + + [Theory, CombinatorialData] + public Task FileBasedApps_Property_01(TestHost testHost) + => TestAsync(""" + #:property LangVersion=preview + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("property"), + Identifier("LangVersion"), + Punctuation.Text("="), + String("preview")); + + // property: no = separator, name only + [Theory, CombinatorialData] + public Task FileBasedApps_Property_02(TestHost testHost) + => TestAsync(""" + #:property LangVersion + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("property"), + Identifier("LangVersion")); + + // property: = separator with no value + [Theory, CombinatorialData] + public Task FileBasedApps_Property_03(TestHost testHost) + => TestAsync(""" + #:property LangVersion= + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("property"), + Identifier("LangVersion"), + Punctuation.Text("=")); + + // property: duplicate = separators + [Theory, CombinatorialData] + public Task FileBasedApps_Property_04(TestHost testHost) + => TestAsync(""" + #:property Key=Value=Extra + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("property"), + Identifier("Key"), + Punctuation.Text("="), + String("Value=Extra")); + + // property: no name, just = and value + [Theory, CombinatorialData] + public Task FileBasedApps_Property_05(TestHost testHost) + => TestAsync(""" + #:property =preview + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("property"), + Punctuation.Text("="), + String("preview")); + + [Theory, CombinatorialData] + public Task FileBasedApps_Project_01(TestHost testHost) + => TestAsync(""" + #:project ../path/to/lib.csproj + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("project"), + String("../path/to/lib.csproj")); + + [Theory, CombinatorialData] + public Task FileBasedApps_Include_01(TestHost testHost) + => TestAsync(""" + #:include src/**/*.cs + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("include"), + String("src/**/*.cs")); + + [Theory, CombinatorialData] + public Task FileBasedApps_Exclude_01(TestHost testHost) + => TestAsync(""" + #:exclude obj/** + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("exclude"), + String("obj/**")); + + [Theory, CombinatorialData] + public Task FileBasedApps_NoValue_01(TestHost testHost) + => TestAsync(""" + #:sdk + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("sdk")); + + [Theory, CombinatorialData] + public Task FileBasedApps_NoValue_02(TestHost testHost) + => TestAsync(""" + #:package + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("package")); + + [Theory, CombinatorialData] + public Task FileBasedApps_NoValue_03(TestHost testHost) + => TestAsync(""" + #:property + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("property")); + + [Theory, CombinatorialData] + public Task FileBasedApps_Unknown_01(TestHost testHost) + => TestAsync(""" + #:unknown // comment + Console.Write(); + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("unknown"), + String("// comment"), + Identifier("Console"), + Operators.Dot, + Identifier("Write"), + Punctuation.OpenParen, + Punctuation.CloseParen, + Punctuation.Semicolon); + + [Theory, CombinatorialData] + public Task FileBasedApps_Unknown_02(TestHost testHost) + => TestAsync(""" + #:no-space + Console.Write(); + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("no-space"), + Identifier("Console"), + Operators.Dot, + Identifier("Write"), + Punctuation.OpenParen, + Punctuation.CloseParen, + Punctuation.Semicolon); + + // Space between `#` and `:` causes the directive to not be treated as an ignored/FBA directive. + [Theory, CombinatorialData] + public Task FileBasedApps_Spaces_01(TestHost testHost) + => TestAsync(""" + # : property name=value + """, + testHost, + PPKeyword("#"), + PPText(": property name=value")); + + // Space between `#:` and kind is allowed. + [Theory, CombinatorialData] + public Task FileBasedApps_Spaces_02(TestHost testHost) + => TestAsync(""" + #: property name = value + """, + testHost, + PPKeyword("#"), + PPKeyword(":"), + PPKeyword("property"), + Identifier("name "), + PunctuationText("="), + String(" value")); +} diff --git a/src/EditorFeatures/CSharpTest/CodeActions/GenerateType/GenerateTypeTests.cs b/src/EditorFeatures/CSharpTest/CodeActions/GenerateType/GenerateTypeTests.cs index 9db8378b51b0..950a8a64e947 100644 --- a/src/EditorFeatures/CSharpTest/CodeActions/GenerateType/GenerateTypeTests.cs +++ b/src/EditorFeatures/CSharpTest/CodeActions/GenerateType/GenerateTypeTests.cs @@ -5915,4 +5915,114 @@ public Student(string v) } } """, index: 1); + + #region Generate Type in Cref + + [Fact] + public Task TestGenerateTypeInTypeCref() + { + return TestInRegularAndScriptAsync( + """ + /// + class C + { + } + """, + """ + /// + class C + { + } + + internal class MissingType + { + } + """, + index: 1, + parameters: new TestParameters(parseOptions: new CSharpParseOptions(documentationMode: DocumentationMode.Diagnose))); + } + + [Fact] + public Task TestGenerateNestedTypeInTypeCref() + { + return TestInRegularAndScriptAsync( + """ + /// + class C + { + } + """, + """ + /// + class C + { + private class MissingType + { + } + } + """, + index: 2, + parameters: new TestParameters(parseOptions: new CSharpParseOptions(documentationMode: DocumentationMode.Diagnose))); + } + + [Fact] + public Task TestGenerateTypeInCrefOnMethod() + { + return TestInRegularAndScriptAsync( + """ + class C + { + /// + public void M() + { + } + } + """, + """ + class C + { + /// + public void M() + { + } + + private class MissingType + { + } + } + """, + index: 2, + parameters: new TestParameters(parseOptions: new CSharpParseOptions(documentationMode: DocumentationMode.Diagnose))); + } + + [Fact] + public Task TestGenerateTypeInCrefWithExplicitTypePrefix() + { + return TestMissingAsync( + """ + /// + class C + { + } + """, + parameters: new TestParameters(parseOptions: new CSharpParseOptions(documentationMode: DocumentationMode.Diagnose))); + } + + [Fact] + public Task TestGenerateTypeInCrefWithExplicitTypePrefixOnMethod() + { + return TestMissingAsync( + """ + class C + { + /// + public void M() + { + } + } + """, + parameters: new TestParameters(parseOptions: new CSharpParseOptions(documentationMode: DocumentationMode.Diagnose))); + } + + #endregion } diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AppDirectiveCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AppDirectiveCompletionProviderTests.cs index ecb5e4713889..6f4a98fa8b0b 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AppDirectiveCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AppDirectiveCompletionProviderTests.cs @@ -126,6 +126,145 @@ public async Task PathRecommendation_03() // Therefore we do not have "negative tests" here for file names. } +public sealed class IncludeAppDirectiveCompletionProviderTests : AbstractAppDirectiveCompletionProviderTests +{ + protected override string DirectiveKind => "include"; + + internal override Type GetCompletionProviderType() + => typeof(IncludeAppDirectiveCompletionProvider); + + [Fact] + public async Task PathRecommendation_01() + { + using var tempRoot = new TempRoot(); + var tempDirectory = tempRoot.CreateDirectory(); + var nestedDirectory = tempDirectory.CreateDirectory("SubDirectory"); + var scriptFilePath = Path.Combine(tempDirectory.Path, "App.cs"); + var code = """ + #:include $$ + """; + var markup = $""" + + + + + + """; + + await VerifyItemExistsAsync(markup, expectedItem: "SubDirectory"); + await VerifyItemExistsAsync(markup, expectedItem: "*.cs"); + await VerifyItemExistsAsync(markup, expectedItem: "**/*.cs"); + } + + [Fact] + public async Task PathRecommendation_02() + { + using var tempRoot = new TempRoot(); + var tempDirectory = tempRoot.CreateDirectory(); + var nestedDirectory = tempDirectory.CreateDirectory("SubDirectory"); + var utilFile = nestedDirectory.CreateFile("Util.cs"); + utilFile.WriteAllText(""" + public class Util { } + """); + + var scriptFilePath = Path.Combine(tempDirectory.Path, "App.cs"); + var code = """ + #:include SubDirectory/$$ + """; + var markup = $""" + + + + + + """; + await VerifyItemExistsAsync(markup, expectedItem: "Util.cs"); + await VerifyItemExistsAsync(markup, expectedItem: "*.cs"); + await VerifyItemExistsAsync(markup, expectedItem: "**/*.cs"); + } + + [Fact] + public async Task PathRecommendation_Virtual_01() + { + // Test a virtual file scenario (e.g. ctrl+N in VS Code or other cases where there is not an actual file on disk.) + var code = """ + #:include $$ + """; + + var markup = $""" + + + + + + """; + + // In this case, only stuff like drive roots would be recommended. + var expectedRoot = PlatformInformation.IsWindows ? "C:" : "/"; + await VerifyItemExistsAsync(markup, expectedRoot); + await VerifyItemIsAbsentAsync(markup, expectedItem: "*.cs"); + await VerifyItemIsAbsentAsync(markup, expectedItem: "**/*.cs"); + } + + [Fact] + public async Task PathRecommendation_Virtual_02() + { + // Test a virtual file scenario with an absolute path + var root = PlatformInformation.IsWindows ? "C:/temp" : "/temp"; + var code = $""" + #:include {root}$$ + """; + + var markup = $""" + + + + + + """; + + await VerifyItemExistsAsync(markup, expectedItem: "*.cs"); + await VerifyItemExistsAsync(markup, expectedItem: "**/*.cs"); + } + + [Fact] + public async Task PathRecommendation_UnknownFileType() + { + using var tempRoot = new TempRoot(); + var tempDirectory = tempRoot.CreateDirectory(); + var nestedDirectory = tempDirectory.CreateDirectory("SubDirectory"); + var utilFile = nestedDirectory.CreateFile("UNKNOWN"); + utilFile.WriteAllText(""" + public class Util { } + """); + + var scriptFilePath = Path.Combine(tempDirectory.Path, "App.cs"); + var code = """ + #:include SubDirectory/$$ + """; + // Note: today, the completion provider doesn't actually respect this. + // In future, we might want to use `FileBasedProgramsItemMapping` to restrict what file extensions we show in completion. + var globalAnalyzerConfig = """ + is_global = true + build_property.FileBasedProgramsItemMapping = .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content + """; + var markup = $""" + + + + + + + """; + await VerifyItemExistsAsync(markup, expectedItem: "UNKNOWN"); + await VerifyItemExistsAsync(markup, expectedItem: "*.cs"); + await VerifyItemExistsAsync(markup, expectedItem: "**/*.cs"); + } + + // Note: The editor uses a shared mechanism to filter out completion items which don't match the prefix of what the user is typing. + // Therefore we do not have "negative tests" here for file names. +} + public abstract class AbstractAppDirectiveCompletionProviderTests : AbstractCSharpCompletionProviderTests { /// The directive kind. For example, `package` in `#:package MyNugetPackage@Version`. diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CompletionProviderOrderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CompletionProviderOrderTests.cs index 9e6b01a1c433..9e6f341bf2db 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CompletionProviderOrderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CompletionProviderOrderTests.cs @@ -76,6 +76,7 @@ public void TestCompletionProviderOrder() typeof(PropertyAppDirectiveCompletionProvider), typeof(PackageAppDirectiveCompletionProvider), typeof(ProjectAppDirectiveCompletionProvider), + typeof(IncludeAppDirectiveCompletionProvider), // Marker for end of built-in completion providers typeof(LastBuiltInCompletionProvider), diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs index d98d6a5fc4b9..73a12953e1b3 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/SymbolCompletionProviderTests.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Completion.Providers; +using Microsoft.CodeAnalysis.CSharp.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders; using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; using Microsoft.CodeAnalysis.Test.Utilities; @@ -14271,6 +14272,58 @@ void M(string s) sourceCodeKind: SourceCodeKind.Regular, glyph: Glyph.ExtensionMethodPublic); + [Theory] + [InlineData("""public int Number => 0;""", + Glyph.PropertyPublic, + """ + extension(TestClass testclass) + { + public int Number() => 0; + } + """, + Glyph.ExtensionMethodPublic)] + [InlineData("""public int Number => 0;""", + Glyph.PropertyPublic, + """public static int Number(this TestClass testclass) => 0;""", + Glyph.ExtensionMethodPublic)] + [InlineData("""public int Number() => 0;""", + Glyph.MethodPublic, + """ + extension(TestClass testclass) + { + public int Number => 0; + } + """, + Glyph.PropertyPublic)] + [WorkItem("https://github.com/dotnet/roslyn/issues/70537")] + internal Task TestIdenticalNameWithExtensionMembersOfDifferentKind(string member, Glyph memberGlyph, string extension, Glyph extensionGlyph) + => VerifyExpectedItemsAsync( + MakeMarkup($$""" + public class TestClass + { + {{member}} + } + + public static class Extensions + { + {{extension}} + } + + internal class Program + { + static void Main(string[] args) + { + var t = new TestClass(); + var x = t.$$ + } + } + """, LanguageVersion.CSharp14), + results: [ + new ItemExpectation(Name: "Number", IsAbsent: false, Glyph: memberGlyph), + new ItemExpectation(Name: "Number", IsAbsent: false, Glyph: extensionGlyph), + ], + sourceCodeKind: SourceCodeKind.Regular); + private static string MakeMarkup( [StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string source, LanguageVersion languageVersion = LanguageVersion.Preview) @@ -14331,4 +14384,51 @@ class Foo2 { } } """, commitChar: commitChar, sourceCodeKind: SourceCodeKind.Regular); } + + [Fact] + public async Task TestTypeCompletionInUnionParameterList_01() + { + await VerifyItemExistsAsync(GetMarkup(""" + class Dog { } + class Cat { } + union Pet($$) + """, LanguageVersionExtensions.CSharpNext), "Dog"); + } + + [Fact] + public async Task TestTypeCompletionInUnionParameterList_02() + { + await VerifyItemExistsAsync(GetMarkup(""" + class Dog { } + class Cat { } + union Pet($$) + """, LanguageVersionExtensions.CSharpNext), "Cat"); + } + + [Fact] + public async Task TestTypeCompletionInUnionParameterList_03() + { + await VerifyItemExistsAsync(GetMarkup(""" + class Dog { } + class Cat { } + union Pet(Dog, $$) + """, LanguageVersionExtensions.CSharpNext), "Cat"); + } + + [Fact] + public async Task TestTypeCompletionInUnionParameterList_04() + { + await VerifyItemExistsAsync(GetMarkup(""" + union U($$) + """, LanguageVersionExtensions.CSharpNext), "System"); + } + + [Fact] + public async Task TestTypeCompletionInUnionParameterList_05() + { + // Type parameter completion in generic union parameter list + await VerifyItemExistsAsync(GetMarkup(""" + union U(T1, $$) + """, LanguageVersionExtensions.CSharpNext), "T2"); + } } diff --git a/src/EditorFeatures/CSharpTest/Formatting/CodeCleanupTests.cs b/src/EditorFeatures/CSharpTest/Formatting/CodeCleanupTests.cs index 8467eae800af..433e550eac98 100644 --- a/src/EditorFeatures/CSharpTest/Formatting/CodeCleanupTests.cs +++ b/src/EditorFeatures/CSharpTest/Formatting/CodeCleanupTests.cs @@ -791,6 +791,54 @@ public Task DoNotRunThirdPartyFixerIfItDoesNotSupportDocumentScope() public Task DoNotApplyFixerIfChangesAreMadeOutsideDocument() => TestThirdPartyCodeFixerNoChanges(_code); + [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2564885")] + public Task DoNotImplementInterfaceMembersDuringCodeCleanup() + => AssertCodeCleanupResult( + expected: """ + internal interface IFoo + { + void Foo(); + } + + internal class Bar : IFoo + { + } + """, + code: """ + internal interface IFoo + { + void Foo(); + } + + internal class Bar : IFoo + { + } + """); + + [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2564885")] + public Task DoNotImplementAbstractMembersDuringCodeCleanup() + => AssertCodeCleanupResult( + expected: """ + internal abstract class Foo + { + internal abstract void M(); + } + + internal class Bar : Foo + { + } + """, + code: """ + internal abstract class Foo + { + internal abstract void M(); + } + + internal class Bar : Foo + { + } + """); + private static Task TestThirdPartyCodeFixerNoChanges(string code, DiagnosticSeverity severity = DiagnosticSeverity.Warning) where TAnalyzer : DiagnosticAnalyzer, new() where TCodefix : CodeFixProvider, new() diff --git a/src/EditorFeatures/CSharpTest/GoToAdjacentMember/CSharpGoToAdjacentMemberTests.cs b/src/EditorFeatures/CSharpTest/GoToAdjacentMember/CSharpGoToAdjacentMemberTests.cs index 55ae362cba69..3972ece508a7 100644 --- a/src/EditorFeatures/CSharpTest/GoToAdjacentMember/CSharpGoToAdjacentMemberTests.cs +++ b/src/EditorFeatures/CSharpTest/GoToAdjacentMember/CSharpGoToAdjacentMemberTests.cs @@ -36,7 +36,7 @@ class C """, next: true)); } - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task BeforeClassWithMember() => AssertNavigatedAsync(""" $$ @@ -46,7 +46,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task AfterClassWithMember() => AssertNavigatedAsync(""" class C @@ -57,7 +57,7 @@ class C $$ """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task BetweenClasses() => AssertNavigatedAsync(""" class C1 @@ -73,7 +73,7 @@ class C2 } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task BetweenClassesPrevious() => AssertNavigatedAsync(""" class C1 @@ -89,7 +89,7 @@ void M() { } } """, next: false); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task FromFirstMemberToSecond() => AssertNavigatedAsync(""" class C @@ -99,7 +99,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task FromSecondToFirst() => AssertNavigatedAsync(""" class C @@ -109,7 +109,7 @@ class C } """, next: false); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task NextWraps() => AssertNavigatedAsync(""" class C @@ -119,7 +119,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task PreviousWraps() => AssertNavigatedAsync(""" class C @@ -129,7 +129,7 @@ class C } """, next: false); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task DescendsIntoNestedType() => AssertNavigatedAsync(""" class C @@ -143,7 +143,7 @@ class N } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtConstructor() => AssertNavigatedAsync(""" class C @@ -153,7 +153,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtDestructor() => AssertNavigatedAsync(""" class C @@ -163,7 +163,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtOperator() => AssertNavigatedAsync(""" class C @@ -172,7 +172,7 @@ class C [||]static C operator+(C left, C right) { throw new System.NotImplementedException(); } } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtField() => AssertNavigatedAsync(""" class C @@ -182,7 +182,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtFieldlikeEvent() => AssertNavigatedAsync(""" class C @@ -192,7 +192,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtAutoProperty() => AssertNavigatedAsync(""" class C @@ -202,7 +202,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtPropertyWithAccessors() => AssertNavigatedAsync(""" class C @@ -217,7 +217,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task SkipsPropertyAccessors() => AssertNavigatedAsync(""" class C @@ -234,7 +234,7 @@ void M1() { } } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task FromInsideAccessor() => AssertNavigatedAsync(""" class C @@ -251,7 +251,7 @@ int P } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtIndexerWithAccessors() => AssertNavigatedAsync(""" class C @@ -266,7 +266,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task SkipsIndexerAccessors() => AssertNavigatedAsync(""" class C @@ -283,7 +283,7 @@ void M1() { } } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtEventWithAddRemove() => AssertNavigatedAsync(""" class C @@ -298,7 +298,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task SkipsEventAddRemove() => AssertNavigatedAsync(""" class C @@ -315,7 +315,7 @@ void M1() { } } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task FromInsideMethod() => AssertNavigatedAsync(""" class C @@ -329,7 +329,7 @@ void M1() } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task NextFromBetweenMethods() => AssertNavigatedAsync(""" class C @@ -342,7 +342,7 @@ void M1() { } } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task PreviousFromBetweenMethods() => AssertNavigatedAsync(""" class C @@ -355,7 +355,7 @@ void M2() { } } """, next: false); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task NextFromBetweenMethodsInTrailingTrivia() => AssertNavigatedAsync(""" class C @@ -368,7 +368,7 @@ void M1() } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task PreviousFromBetweenMethodsInTrailingTrivia() => AssertNavigatedAsync(""" class C @@ -381,7 +381,7 @@ void M2() { } } """, next: false); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task StopsAtExpressionBodiedMember() => AssertNavigatedAsync(""" class C @@ -392,7 +392,7 @@ class C } """, next: true); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] [WorkItem("https://github.com/dotnet/roslyn/issues/10588")] public Task PreviousFromInsideCurrent() => AssertNavigatedAsync(""" @@ -409,7 +409,7 @@ void M2() } """, next: false); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task NextInScript() => AssertNavigatedAsync(""" $$void M1() { } @@ -417,11 +417,31 @@ public Task NextInScript() [||]void M2() { } """, next: true, sourceCodeKind: SourceCodeKind.Script); - [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/4311")] public Task PrevInScript() => AssertNavigatedAsync(""" [||]void M1() { } $$void M2() { } """, next: false, sourceCodeKind: SourceCodeKind.Script); + + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/77393")] + public Task SelectionNextMethod() + => AssertNavigatedAsync(""" + class C + { + {|selection:$$void M1() { }|} + [||]void M2() { } + } + """, next: true); + + [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/77393")] + public Task SelectionPreviousMethod() + => AssertNavigatedAsync(""" + class C + { + [||]void M1() { } + {|selection:$$void M2() { }|} + } + """, next: false); } diff --git a/src/EditorFeatures/CSharpTest/Interactive/NavigateTo/InteractiveNavigateToTests.cs b/src/EditorFeatures/CSharpTest/Interactive/NavigateTo/InteractiveNavigateToTests.cs index 18b3038afc18..ed25c4a661a8 100644 --- a/src/EditorFeatures/CSharpTest/Interactive/NavigateTo/InteractiveNavigateToTests.cs +++ b/src/EditorFeatures/CSharpTest/Interactive/NavigateTo/InteractiveNavigateToTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -628,21 +628,33 @@ public Task TermSplittingTest5(TestHost testHost, Composition composition) public Task TermSplittingTest7(TestHost testHost, Composition composition) => TestAsync(testHost, composition, "class SyllableBreaking {int GetKeyWord; int get_key_word; string get_keyword; int getkeyword; int wake;}", async w => { - var expecteditem1 = new NavigateToItem("get_key_word", NavigateToItemKind.Field, "csharp", null, null, s_emptyCamelCaseSubstringPatternMatch_NotCaseSensitive, null); - var expecteditem2 = new NavigateToItem("GetKeyWord", NavigateToItemKind.Field, "csharp", null, null, s_emptySubstringPatternMatch, null); - var expecteditems = new List { expecteditem1, expecteditem2 }; - - var items = await _aggregator.GetItemsAsync("K*W"); - - VerifyNavigateToResultItems(expecteditems, items); + var items = await _aggregator.GetItemsAsync("GTW"); + Assert.Empty(items); }); [WpfTheory] [CombinatorialData] - public Task TermSplittingTest8(TestHost testHost, Composition composition) + public Task TermSplittingTest_DotStarRegex_NoExtractableLiterals(TestHost testHost, Composition composition) => TestAsync(testHost, composition, "class SyllableBreaking {int GetKeyWord; int get_key_word; string get_keyword; int getkeyword; int wake;}", async w => { - var items = await _aggregator.GetItemsAsync("GTW"); + // G.*W is a valid regex but G and W are single-char literals — no 2+ char literals + // can be extracted for pre-filtering, so the regex search is skipped entirely. + var items = await _aggregator.GetItemsAsync("G.*W"); Assert.Empty(items); }); + + [WpfTheory] + [CombinatorialData] + public Task TermSplittingTest_DotStarRegex_WithExtractableLiterals(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, "class SyllableBreaking {int GetKeyWord; int get_key_word; string get_keyword; int getkeyword; int wake;}", async w => + { + // Ge.*Wo has 2+ char literals ("ge", "wo") so the regex search runs. + // Matches any symbol containing "Ge" followed eventually by "Wo" (case-insensitive). + var items = await _aggregator.GetItemsAsync("Ge.*Wo"); + Assert.Equal(4, items.Count()); + Assert.Contains(items, i => i.Name == "GetKeyWord"); + Assert.Contains(items, i => i.Name == "get_key_word"); + Assert.Contains(items, i => i.Name == "get_keyword"); + Assert.Contains(items, i => i.Name == "getkeyword"); + }); } diff --git a/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToRegexTests.cs b/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToRegexTests.cs new file mode 100644 index 000000000000..9d8eedca7443 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToRegexTests.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.UnitTests.NavigateTo; +using Microsoft.CodeAnalysis.Remote.Testing; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.VisualStudio.Language.NavigateTo.Interfaces; +using Microsoft.VisualStudio.Text.PatternMatching; +using Roslyn.Test.EditorUtilities.NavigateTo; +using Roslyn.Test.Utilities; +using Xunit; + +#pragma warning disable CS0618 // MatchKind is obsolete + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.NavigateTo; + +[Trait(Traits.Feature, Traits.Features.NavigateTo)] +public sealed class NavigateToRegexTests : AbstractNavigateToTests +{ + protected override string Language => "csharp"; + + protected override EditorTestWorkspace CreateWorkspace(string content, TestComposition composition) + => EditorTestWorkspace.CreateCSharp(content, composition: composition); + + private const string MultiSymbolSource = """ + namespace MyNamespace + { + class ReadLine { } + class WriteLine { } + class StreamReader { } + class StreamWriter { } + class GooBar { } + class BazQuux { } + class MyGooBarEnd { } + } + """; + + #region Alternation + + [Theory, CombinatorialData] + public Task Regex_Alternation_MatchesFirstBranch(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("(Read|Write)Line"); + Assert.Equal(2, items.Count()); + + var readLine = items.Single(i => i.Name == "ReadLine"); + VerifyNavigateToResultItem(readLine, "ReadLine", "[|ReadLine|]", PatternMatchKind.Exact, NavigateToItemKind.Class, Glyph.ClassInternal); + + var writeLine = items.Single(i => i.Name == "WriteLine"); + VerifyNavigateToResultItem(writeLine, "WriteLine", "[|WriteLine|]", PatternMatchKind.Exact, NavigateToItemKind.Class, Glyph.ClassInternal); + }); + + [Theory, CombinatorialData] + public Task Regex_Alternation_NoMatchWhenNoBranchMatches(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("(Delete|Append)Line"); + Assert.Empty(items); + }); + + #endregion + + #region Wildcards + + [Theory, CombinatorialData] + public Task Regex_DotStar_MatchesSubstring(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("Goo.*Bar"); + Assert.Equal(2, items.Count()); + + var gooBar = items.Single(i => i.Name == "GooBar"); + VerifyNavigateToResultItem(gooBar, "GooBar", "[|GooBar|]", PatternMatchKind.Exact, NavigateToItemKind.Class, Glyph.ClassInternal); + + var myGooBarEnd = items.Single(i => i.Name == "MyGooBarEnd"); + VerifyNavigateToResultItem(myGooBarEnd, "MyGooBarEnd", "My[|GooBar|]End", PatternMatchKind.Substring, NavigateToItemKind.Class, Glyph.ClassInternal); + }); + + [Theory, CombinatorialData] + public Task Regex_DotStar_NoMatchWhenLiteralsAbsent(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("Alpha.*Beta"); + Assert.Empty(items); + }); + + #endregion + + #region Case sensitivity + + [Theory, CombinatorialData] + public Task Regex_CaseInsensitive_FindsMatch(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("readline"); + var item = items.Single(i => i.Name == "ReadLine"); + Assert.Equal(PatternMatchKind.Exact, item.PatternMatch.Kind); + Assert.False(item.PatternMatch.IsCaseSensitive); + }); + + [Theory, CombinatorialData] + public Task Regex_CaseSensitive_MatchReportedCorrectly(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("(Read|Write)Line"); + var item = items.Single(i => i.Name == "ReadLine"); + Assert.Equal(PatternMatchKind.Exact, item.PatternMatch.Kind); + Assert.True(item.PatternMatch.IsCaseSensitive); + }); + + #endregion + + #region Character classes + + [Theory, CombinatorialData] + public Task Regex_CharacterClass_Matches(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("Stream[RW]"); + Assert.Equal(2, items.Count()); + + var reader = items.Single(i => i.Name == "StreamReader"); + VerifyNavigateToResultItem(reader, "StreamReader", "[|StreamR|]eader", PatternMatchKind.Substring, NavigateToItemKind.Class, Glyph.ClassInternal); + + var writer = items.Single(i => i.Name == "StreamWriter"); + VerifyNavigateToResultItem(writer, "StreamWriter", "[|StreamW|]riter", PatternMatchKind.Substring, NavigateToItemKind.Class, Glyph.ClassInternal); + }); + + #endregion + + #region Anchored patterns + + [Theory, CombinatorialData] + public Task Regex_AnchoredExact_MatchesFullName(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, """ + class GooBar { } + class MyGooBar { } + """, async w => + { + var items = await _aggregator.GetItemsAsync("^GooBar$"); + var item = items.Single(); + Assert.Equal("GooBar", item.Name); + Assert.Equal(PatternMatchKind.Exact, item.PatternMatch.Kind); + }); + + #endregion + + #region Container.Name splitting + + [Theory, CombinatorialData] + public Task Regex_ContainerDotName_SplitsCorrectly(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, """ + namespace MyNamespace + { + class Target { } + } + """, async w => + { + var items = await _aggregator.GetItemsAsync("MyNamespace.Target"); + var item = items.Single(i => i.Name == "Target"); + Assert.Equal(PatternMatchKind.Exact, item.PatternMatch.Kind); + }); + + [Theory, CombinatorialData] + public Task Regex_ContainerRegex_DotName(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, """ + namespace Alpha + { + class Target { } + } + namespace Beta + { + class Target { } + } + namespace Gamma + { + class Other { } + } + """, async w => + { + var items = await _aggregator.GetItemsAsync("(Alpha|Beta).Target"); + Assert.Equal(2, items.Count()); + Assert.All(items, i => Assert.Equal("Target", i.Name)); + }); + + #endregion + + #region Whitespace stripping + + [Theory, CombinatorialData] + public Task Regex_WhitespaceInPattern_IsIgnored(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("( Read | Write ) Line"); + Assert.Equal(2, items.Count()); + Assert.Contains(items, i => i.Name == "ReadLine"); + Assert.Contains(items, i => i.Name == "WriteLine"); + }); + + #endregion + + #region Invalid regex + + [Theory, CombinatorialData] + public Task Regex_InvalidPattern_ProducesNoResults(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("(unclosed"); + Assert.Empty(items); + }); + + #endregion + + #region Negative — no false positives from regex path + + [Theory, CombinatorialData] + public Task Regex_NoMatch_WhenPatternDoesNotMatchAnySymbol(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("^ZzzNotPresent$"); + Assert.Empty(items); + }); + + [Theory, CombinatorialData] + public Task Regex_NoMatch_WhenAlternationMisses(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("(Alpha|Beta)Line"); + Assert.Empty(items); + }); + + #endregion + + #region Mixed regex and non-regex + + [Theory, CombinatorialData] + public Task NonRegex_PlainText_StillWorks(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, MultiSymbolSource, async w => + { + var items = await _aggregator.GetItemsAsync("GooBar"); + var item = items.Single(i => i.Name == "GooBar"); + Assert.Equal(PatternMatchKind.Exact, item.PatternMatch.Kind); + }); + + [Theory, CombinatorialData] + public Task NonRegex_DotSeparated_StillWorks(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, """ + namespace MyNamespace + { + class Target { } + } + """, async w => + { + var item = (await _aggregator.GetItemsAsync("MyNamespace.Target")).Single(i => i.Name == "Target"); + Assert.Equal(PatternMatchKind.Exact, item.PatternMatch.Kind); + }); + + #endregion + + #region Quantifiers + + [Theory, CombinatorialData] + public Task Regex_OneOrMore_Matches(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, """ + class GoBar { } + class GooBar { } + class GoooBar { } + """, async w => + { + var items = await _aggregator.GetItemsAsync("Go+Bar"); + Assert.Equal(3, items.Count()); + }); + + [Theory, CombinatorialData] + public Task Regex_ZeroOrMore_MatchesZeroOccurrences(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, """ + class GBar { } + class GoBar { } + """, async w => + { + var items = await _aggregator.GetItemsAsync("Go*Bar"); + Assert.Equal(2, items.Count()); + Assert.Contains(items, i => i.Name == "GBar"); + }); + + #endregion +} diff --git a/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToTests.cs b/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToTests.cs index e865d72f847d..095c915629ce 100644 --- a/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToTests.cs +++ b/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToTests.cs @@ -93,6 +93,30 @@ await TestAsync(testHost, composition, content, async w => }); } + [Theory, CombinatorialData] + public async Task FindUnion(TestHost testHost, Composition composition) + { + // Exercises the full NavigateTo pipeline for union declarations + var content = XElement.Parse(""" + + + + union Goo(int) + { + } + + + + """); + await TestAsync(testHost, composition, content, async w => + { + // Tracked by https://github.com/dotnet/roslyn/issues/82607 + // Consider using separate NavigateToItemKind and Glyph for unions + var item = (await _aggregator.GetItemsAsync("Goo")).Single(x => x.Kind != "Method"); + VerifyNavigateToResultItem(item, "Goo", "[|Goo|]", PatternMatchKind.Exact, NavigateToItemKind.Structure, Glyph.StructureInternal); + }); + } + [Theory, CombinatorialData] public async Task FindClassInFileScopedNamespace(TestHost testHost, Composition composition) { @@ -1003,6 +1027,30 @@ public Task TermSplittingTest7(TestHost testHost, Composition composition) Assert.Empty(items); }); + [Theory, CombinatorialData] + public Task TermSplittingTest_DotStarRegex_NoExtractableLiterals(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, "class SyllableBreaking {int GetKeyWord; int get_key_word; string get_keyword; int getkeyword; int wake;}", async w => + { + // G.*W is a valid regex but G and W are single-char literals — no 2+ char literals + // can be extracted for pre-filtering, so the regex search is skipped entirely. + var items = await _aggregator.GetItemsAsync("G.*W"); + Assert.Empty(items); + }); + + [Theory, CombinatorialData] + public Task TermSplittingTest_DotStarRegex_WithExtractableLiterals(TestHost testHost, Composition composition) + => TestAsync(testHost, composition, "class SyllableBreaking {int GetKeyWord; int get_key_word; string get_keyword; int getkeyword; int wake;}", async w => + { + // Ge.*Wo has 2+ char literals ("ge", "wo") so the regex search runs. + // Matches any symbol containing "Ge" followed eventually by "Wo" (case-insensitive). + var items = await _aggregator.GetItemsAsync("Ge.*Wo"); + Assert.Equal(4, items.Count()); + Assert.Contains(items, i => i.Name == "GetKeyWord"); + Assert.Contains(items, i => i.Name == "get_key_word"); + Assert.Contains(items, i => i.Name == "get_keyword"); + Assert.Contains(items, i => i.Name == "getkeyword"); + }); + [Theory, CombinatorialData] public Task TestIndexer1(TestHost testHost, Composition composition) => TestAsync(testHost, composition, """ @@ -1792,5 +1840,114 @@ await TestAsync(testHost, composition, content, async w => VerifyNavigateToResultItem(item, "Goo", "[|Goo|]", PatternMatchKind.Exact, NavigateToItemKind.Property, Glyph.PropertyPublic); }); } + + #region Pre-filter integration tests + // These tests verify that the NavigateToSearchInfo pre-filter (hump bigrams, trigram filter, + // length bitset, and the split fuzzy/non-fuzzy signal) correctly allows matches through the + // full NavigateTo pipeline. Exhaustive unit tests for each individual filter are in + // TopLevelSyntaxTreeIndexTests (src/Workspaces/CoreTest/FindSymbols/TopLevelSyntaxTreeIndexTests.cs). + + /// + /// Verifies that a CamelCase pattern passes the hump bigram filter and produces a match. + /// The pre-filter stores ordered pairs of adjacent hump initials (e.g., "GB" for GooBar) + /// and the DP checks against them. "GB" should match "GooBar" as CamelCaseExact. + /// + [Theory, CombinatorialData] + public Task PreFilter_CamelCaseHumpBigram(TestHost testHost, Composition composition) + => TestAsync( + testHost, composition, """ + class GooBar + { + } + """, async w => + { + var item = (await _aggregator.GetItemsAsync("GB")).Single(x => x.Kind != "Method"); + VerifyNavigateToResultItem(item, "GooBar", "[|G|]oo[|B|]ar", PatternMatchKind.CamelCaseExact, NavigateToItemKind.Class, Glyph.ClassInternal); + }); + + /// + /// Verifies that an all-lowercase pattern passes the hump prefix filter via the DP algorithm. + /// The pre-filter stores lowercased prefixes of each hump (e.g., "g", "go", "goo" from "Goo"). + /// The DP tries to split the all-lowercase pattern into segments that are each a valid prefix + /// of some hump. "goo" matches the "Goo" hump prefix, so the filter lets it through. + /// PatternMatcher returns Prefix (not CamelCasePrefix) because "goo" is a literal + /// case-insensitive prefix of "GooBar" and literal prefix takes priority. + /// + [Theory, CombinatorialData] + public Task PreFilter_AllLowercaseDPHumpPrefix(TestHost testHost, Composition composition) + => TestAsync( + testHost, composition, """ + class GooBar + { + } + """, async w => + { + var item = (await _aggregator.GetItemsAsync("goo")).Single(x => x.Kind != "Method"); + VerifyNavigateToResultItem(item, "GooBar", "[|Goo|]Bar", PatternMatchKind.Prefix, NavigateToItemKind.Class, Glyph.ClassInternal); + }); + + /// + /// Verifies that a lowercase substring pattern passes the trigram filter and produces a match. + /// The pre-filter stores all 3-char sliding windows of the lowercased symbol name. The pattern + /// "line" has trigrams "lin" and "ine", both present in "readline", so the filter lets it through. + /// The PatternMatcher returns because "line" + /// is all-lowercase and lands at a non-word-boundary in "Readline". NavigateToMatchKind has no + /// dedicated bucket for this, so it maps to Fuzzy (see s_kindPairs). Exhaustive coverage of the + /// underlying PatternMatchKind is in NavigateToSearchIndexTests. + /// + [Theory, CombinatorialData] + public Task PreFilter_TrigramSubstring(TestHost testHost, Composition composition) + => TestAsync( + testHost, composition, """ + class C + { + void Readline() { } + } + """, async w => + { + var item = (await _aggregator.GetItemsAsync("line")).Single(); + VerifyNavigateToResultItem(item, "Readline", "Read[|line|]()", PatternMatchKind.Fuzzy, NavigateToItemKind.Method, Glyph.MethodPrivate); + }); + + /// + /// Verifies that a fuzzy match is found when the length bitset check passes (symbol length + /// within ±2 of pattern length). With the split fuzzy/non-fuzzy pre-filtering, the length + /// and bigram checks enable the FuzzyPatternMatcher's edit-distance computation. + /// "ToEror" (length 6) fuzzy-matches "ToError" (length 7), delta=1, within ±2. + /// + [Theory, CombinatorialData] + public Task PreFilter_FuzzyMatchEnabledByLengthCheck(TestHost testHost, Composition composition) + => TestAsync( + testHost, composition, """ + class C + { + public void ToError() { } + } + """, async w => + { + var item = (await _aggregator.GetItemsAsync("ToEror")).Single(); + VerifyNavigateToResultItem(item, "ToError", "ToError()", PatternMatchKind.Fuzzy, NavigateToItemKind.Method, Glyph.MethodPublic); + }); + + /// + /// Verifies that a pattern completely unrelated to any symbol in the document produces no + /// results. All three pre-filter checks (hump, trigram, length) must fail for the document + /// to be skipped entirely. "XyzXyzXyzXyz" shares no hump structure, no trigrams, and has + /// length 12 which is far from "GooBar" (length 6). + /// + [Theory, CombinatorialData] + public Task PreFilter_NoMatchWhenAllChecksFail(TestHost testHost, Composition composition) + => TestAsync( + testHost, composition, """ + class GooBar + { + } + """, async w => + { + var items = await _aggregator.GetItemsAsync("XyzXyzXyzXyz"); + Assert.Empty(items); + }); + + #endregion } #pragma warning restore CS0618 // MatchKind is obsolete diff --git a/src/EditorFeatures/CSharpTest/SignatureHelp/InvocationExpressionSignatureHelpProviderTests.cs b/src/EditorFeatures/CSharpTest/SignatureHelp/InvocationExpressionSignatureHelpProviderTests.cs index 4b6b6f026336..7f20c09dca92 100644 --- a/src/EditorFeatures/CSharpTest/SignatureHelp/InvocationExpressionSignatureHelpProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/SignatureHelp/InvocationExpressionSignatureHelpProviderTests.cs @@ -2338,6 +2338,39 @@ public interface IResourceBuilder where T : IResource { } """, [new SignatureHelpTestItem($"({CSharpFeaturesResources.extension}) IResourceBuilder IResourceBuilder.WithServiceBinding(int containerPort, [int? hostPort = null], [string? scheme = null], [string? name = null], [string? env = null])", currentParameterIndex: 0)], sourceCodeKind: SourceCodeKind.Regular); + [Fact] + public Task TestMethodThroughExtensionProperty() + => TestAsync(""" + public enum Foo + { + Bar + } + + public class Description(Foo foo) + { + public void Baz(int a, string b) + { + } + } + + public static class FooEx + { + extension(Foo foo) + { + public Description Description => new(foo); + } + } + + class C + { + void M() + { + [|Foo.Bar.Description.Baz($$ + |]} + } + """, [new SignatureHelpTestItem("void Description.Baz(int a, string b)", currentParameterIndex: 0)], + sourceCodeKind: SourceCodeKind.Regular); + [Fact] public Task TestLightweightOverloadResolution2() => TestAsync( diff --git a/src/EditorFeatures/CSharpTest/StringCopyPaste/PasteUnknownSourceIntoVerbatimStringTests.cs b/src/EditorFeatures/CSharpTest/StringCopyPaste/PasteUnknownSourceIntoVerbatimStringTests.cs index 992ca3b4e4f7..b6ac1841e15c 100644 --- a/src/EditorFeatures/CSharpTest/StringCopyPaste/PasteUnknownSourceIntoVerbatimStringTests.cs +++ b/src/EditorFeatures/CSharpTest/StringCopyPaste/PasteUnknownSourceIntoVerbatimStringTests.cs @@ -110,7 +110,7 @@ public void TestNormalTextIntoVerbatimString() [WpfFact, WorkItem("https://github.com/dotnet/roslyn/issues/62969")] public void TestNormalTextWithSomeQuotesToEscapeAndSomeToNotEscapeIntoVerbatimString() { - // Because we're escaping the quotes in "CA2013", we should also escape teh `""`, even though `""` is legal + // Because we're escaping the quotes in "CA2013", we should also escape the `""`, even though `""` is legal // to have in a verbatim string. TestPasteUnknownSource( pasteText: """ diff --git a/src/EditorFeatures/CSharpTest/Structure/InitializerExpressionStructureTests.cs b/src/EditorFeatures/CSharpTest/Structure/InitializerExpressionStructureTests.cs index 8613d575ab3c..0a11929e8aae 100644 --- a/src/EditorFeatures/CSharpTest/Structure/InitializerExpressionStructureTests.cs +++ b/src/EditorFeatures/CSharpTest/Structure/InitializerExpressionStructureTests.cs @@ -25,10 +25,10 @@ class C { void M() { - var v = {|hint:new Dictionary{|textspan: $${ + {|hint:var v = new Dictionary {|textspan:$${ { 1, 2 }, { 1, 2 }, - }|}|}; + }|};|} } } """, @@ -52,4 +52,43 @@ void M() } """, Region("textspan", "hint", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); + + [Fact] + public Task TestOuterInitializerWithMultiLineArgumentList() + => VerifyBlockSpansAsync( + """ + class C + { + void M() + { + {|hint:using var v = new MailMessage( + fromAddress, + new MailAddress(member.Emails[0].Address)) + {|textspan:$${ + Subject = subject, + Body = plainBody + }|};|} + } + } + """, + Region("textspan", "hint", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); + + [Fact] + public Task TestOuterInitializerWithMultiLineArgumentList_BraceOnSameLine() + => VerifyBlockSpansAsync( + """ + class C + { + void M() + { + {|hint:using var mailMessage = new MailMessage( + DC.Data.FromEMailAddress, + new MailAddress(member.Emails[0].Address)) {|textspan:$${ + Subject = subject, + Body = plainBody + }|};|} + } + } + """, + Region("textspan", "hint", CSharpStructureHelpers.Ellipsis, autoCollapse: false)); } diff --git a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests.cs b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests.cs index 29480db0609a..2feea227fa5f 100644 --- a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests.cs +++ b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests.cs @@ -396,7 +396,7 @@ private object RemoveSequenceNode(XNode node) return new XElement(element.Name, children); } - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = "Deep recursion test relies on Windows stack size (~1MB) to trigger stack overflow; Linux has 8MB stack")] public void TestDeepRecursion1() { var (token, tree, chars) = @@ -447,7 +447,7 @@ public void TestDeepRecursion1() Assert.Null(tree); } - [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_queries/edit/1691963")] + [ConditionalFact(typeof(WindowsOnly), Reason = "Deep recursion test relies on Windows stack size"), WorkItem("https://devdiv.visualstudio.com/DevDiv/_queries/edit/1691963")] public void TestDeepRecursion2() { var (token, tree, chars) = diff --git a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests_BasicTests.cs b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests_BasicTests.cs index c6e919b38312..af3162387a16 100644 --- a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests_BasicTests.cs +++ b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests_BasicTests.cs @@ -1398,7 +1398,7 @@ public void TestMissingColon() """); - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = @"Diagnostic offsets differ due to \r\n vs \n line endings")] public void TestNestedPropertyMissingColon() => Test(""" @" @@ -1609,7 +1609,7 @@ public void TestMissingColon2() """); - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = @"Diagnostic offsets differ due to \r\n vs \n line endings")] public void TestAdditionalContentComma() => Test(""" @"[ @@ -1665,7 +1665,7 @@ public void TestAdditionalContentComma() """); - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = @"Diagnostic offsets differ due to \r\n vs \n line endings")] public void TestAdditionalContentText() => Test(""" @"[ @@ -2975,7 +2975,7 @@ public void TestMultiItemList2() """); - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = @"Diagnostic offsets differ due to \r\n vs \n line endings")] public void TestMultiLine1() => Test(""" @" @@ -3071,7 +3071,7 @@ public void TestMultiLine2() "", ""); - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = @"Diagnostic offsets differ due to \r\n vs \n line endings")] public void TestNestedObject() => Test(""" @" @@ -3525,7 +3525,7 @@ public void TestLiterals2() "", ""); - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = @"Diagnostic offsets differ due to \r\n vs \n line endings")] public void TestLiterals3() => Test(""" @"[ diff --git a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests_NstTests.cs b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests_NstTests.cs index 43ee2d17d388..be7c310eafe8 100644 --- a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests_NstTests.cs +++ b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/Json/CSharpJsonParserTests_NstTests.cs @@ -5,6 +5,7 @@ // tests from: https://github.com/nst/JSONTestSuite using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.Features.EmbeddedLanguages.Json; +using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.EmbeddedLanguages.Json; @@ -1462,7 +1463,7 @@ public void n_array_missing_value_json() """); - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = @"Diagnostic offsets differ due to \r\n vs \n line endings")] public void n_array_newlines_unclosed_json() => TestNST(""" @"[""a"", @@ -1683,7 +1684,7 @@ public void n_array_unclosed_trailing_comma_json() """); - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = @"Diagnostic offsets differ due to \r\n vs \n line endings")] public void n_array_unclosed_with_new_lines_json() => TestNST(""" @"[1, diff --git a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests.cs b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests.cs index 4d7e13d8419e..d99557913278 100644 --- a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests.cs +++ b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -41,11 +41,12 @@ private static SyntaxToken GetStringToken(string text) } private void Test( - string stringText, string expected, RegexOptions options) + string stringText, string expected, RegexOptions options, + bool runtimeHasBug = false) { - var (tree, sourceText) = TryParseTree(stringText, options, conversionFailureOk: false); + var (tree, sourceText) = TryParseTree(stringText, options, conversionFailureOk: false, runtimeHasBug); - TryParseSubTrees(stringText, options); + TryParseSubTrees(stringText, options, runtimeHasBug); var actual = TreeToText(sourceText, tree) .Replace(""", """ @@ -54,7 +55,7 @@ private void Test( AssertEx.Equal(expected, actual); } - private void TryParseSubTrees(string stringText, RegexOptions options) + private void TryParseSubTrees(string stringText, RegexOptions options, bool runtimeHasBug = false) { // Trim the input from the right and make sure tree invariants hold var current = stringText; @@ -67,7 +68,7 @@ private void TryParseSubTrees(string stringText, RegexOptions options) current = current[..^2] + """ " """; - TryParseTree(current, options, conversionFailureOk: true); + TryParseTree(current, options, conversionFailureOk: true, runtimeHasBug); } // Trim the input from the left and make sure tree invariants hold @@ -91,7 +92,7 @@ private void TryParseSubTrees(string stringText, RegexOptions options) """ + current[2..]; } - TryParseTree(current, options, conversionFailureOk: true); + TryParseTree(current, options, conversionFailureOk: true, runtimeHasBug); } for (var start = stringText[0] == '@' ? 2 : 1; start < stringText.Length - 1; start++) @@ -100,7 +101,8 @@ private void TryParseSubTrees(string stringText, RegexOptions options) stringText[..start] + stringText[(start + 1)..], options, - conversionFailureOk: true); + conversionFailureOk: true, + runtimeHasBug); } } @@ -120,7 +122,8 @@ private void TryParseSubTrees(string stringText, RegexOptions options) } private (RegexTree, SourceText) TryParseTree( - string stringText, RegexOptions options, bool conversionFailureOk) + string stringText, RegexOptions options, bool conversionFailureOk, + bool runtimeHasBug = false) { var (token, tree, allChars) = JustParseTree(stringText, options, conversionFailureOk); if (tree == null) @@ -140,6 +143,13 @@ private void TryParseSubTrees(string stringText, RegexOptions options) } catch (ArgumentException ex) { + if (runtimeHasBug) + { + // The .NET runtime has a bug where it rejects this pattern. Our parser correctly + // accepts it. Skip runtime validation. See https://github.com/dotnet/runtime/issues/111633 + return treeAndText; + } + Assert.NotEmpty(tree.Diagnostics); // Ensure the diagnostic we emit is the same as the .NET one. Note: we can only @@ -332,7 +342,7 @@ private static string And(params ReadOnlySpan regexes) private static string Not(string regex) => $"(?({regex})[0-[0]]|.*)"; - [Fact] + [ConditionalFact(typeof(WindowsOnly), Reason = "Deep recursion test relies on Windows stack size (~1MB) to trigger stack overflow; Linux has 8MB stack")] public void TestDeepRecursion() { var (token, tree, chars) = diff --git a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests_BasicTests.cs b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests_BasicTests.cs index a7e8f58ab13b..3a9b5240fd93 100644 --- a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests_BasicTests.cs +++ b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests_BasicTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -18304,4 +18304,367 @@ public void TestMinusAsCharacterClassStart() """, RegexOptions.None); + + [Fact] + public void TestInlineOptionsInConditionalBranch_ExpressionConditional_YesBranch() + => Test(""" + @"(?(cat)(?i)CAT|dog)" + """, """ + + + + + ( + ? + + ( + + + cat + + + ) + + + + + ( + ? + i + ) + + + CAT + + + | + + + dog + + + + ) + + + + + + + + + """, RegexOptions.None, runtimeHasBug: true); + + [Fact] + public void TestInlineOptionsInConditionalBranch_ExpressionConditional_NoBranch() + => Test(""" + @"(?(cat)cat|(?i)dog)" + """, """ + + + + + ( + ? + + ( + + + cat + + + ) + + + + + cat + + + | + + + ( + ? + i + ) + + + dog + + + + ) + + + + + + + + + """, RegexOptions.None, runtimeHasBug: true); + + [Fact] + public void TestInlineOptionsInConditionalBranch_NestedOptionsGroup() + => Test(""" + @"(?(cat)(?i:CAT)|dog)" + """, """ + + + + + ( + ? + + ( + + + cat + + + ) + + + + + ( + ? + i + : + + + CAT + + + ) + + + | + + + dog + + + + ) + + + + + + + + + """, RegexOptions.None, runtimeHasBug: true); + + [Fact] + public void TestInlineOptionsInConditionalBranch_LookaheadConditional() + => Test(""" + @"(?(?=cat)(?i)CAT|dog)" + """, """ + + + + + ( + ? + + ( + ? + = + + + cat + + + ) + + + + + ( + ? + i + ) + + + CAT + + + | + + + dog + + + + ) + + + + + + + + + """, RegexOptions.None, runtimeHasBug: true); + + [Fact] + public void TestInlineOptionsInConditionalBranch_BackreferenceConditional() + => Test(""" + @"(cat)?(?(1)(?i)dog|pig)" + """, """ + + + + + + ( + + + cat + + + ) + + ? + + + ( + ? + ( + 1 + ) + + + + ( + ? + i + ) + + + dog + + + | + + + pig + + + + ) + + + + + + + + + + """, RegexOptions.None, runtimeHasBug: true); + + [Fact] + public void TestInlineOptionsInConditionalBranch_OptionsInTestExpression() + => Test(""" + @"(?((?i)cat)CAT|dog)" + """, """ + + + + + ( + ? + + ( + + + ( + ? + i + ) + + + cat + + + ) + + + + + CAT + + + | + + + dog + + + + ) + + + + + + + + + """, RegexOptions.None, runtimeHasBug: true); + + [ConditionalFact(typeof(IsEnglishLocal))] + public void TestInlineOptionsInConditionalBranch_OptionsAsTestExpression_Negative() + => Test(""" + @"(?(?i)yes|no)" + """, """ + + + + + ( + ? + + ( + + + ? + + + i + + + ) + + + + + yes + + + | + + + no + + + + ) + + + + + + + + + + + + + """, RegexOptions.None); } diff --git a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests_DotnetNegativeTests.cs b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests_DotnetNegativeTests.cs index 95bf97a8b109..ea2776695e93 100644 --- a/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests_DotnetNegativeTests.cs +++ b/src/EditorFeatures/CSharpTest2/EmbeddedLanguages/RegularExpressions/CSharpRegexParserTests_DotnetNegativeTests.cs @@ -5,6 +5,7 @@ #nullable disable using System.Text.RegularExpressions; +using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests.EmbeddedLanguages.RegularExpressions; @@ -1284,7 +1285,7 @@ public void NegativeTest40() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest41() => Test(""" @"(?(?i))" @@ -1356,7 +1357,7 @@ public void NegativeTest42() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest43() => Test(""" @"(?(?I))" @@ -1395,7 +1396,7 @@ public void NegativeTest43() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest44() => Test(""" @"(?(?M))" @@ -1434,7 +1435,7 @@ public void NegativeTest44() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest45() => Test(""" @"(?(?s))" @@ -1473,7 +1474,7 @@ public void NegativeTest45() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest46() => Test(""" @"(?(?S))" @@ -1512,7 +1513,7 @@ public void NegativeTest46() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest47() => Test(""" @"(?(?x))" @@ -1551,7 +1552,7 @@ public void NegativeTest47() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest48() => Test(""" @"(?(?X))" @@ -1590,7 +1591,7 @@ public void NegativeTest48() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest49() => Test(""" @"(?(?n))" @@ -1629,7 +1630,7 @@ public void NegativeTest49() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest50() => Test(""" @"(?(?m))" @@ -2319,7 +2320,7 @@ public void NegativeTest68() """, RegexOptions.None); - [Fact] + [ConditionalFact(typeof(IsEnglishLocal))] public void NegativeTest69() => Test(""" @"(?(?N))" diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/AbstractKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/AbstractKeywordRecommenderTests.cs index 97370453b962..498b4b44bea1 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/AbstractKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/AbstractKeywordRecommenderTests.cs @@ -228,6 +228,14 @@ struct S { $$ """); + [Fact] + public Task TestInsideUnion() + => VerifyKeywordAsync( + """ + union U(int) { + $$ + """, CSharpNextParseOptions, CSharpNextScriptParseOptions); + [Fact] public Task TestInsideInterface() => VerifyKeywordAsync(""" diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/ConstKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/ConstKeywordRecommenderTests.cs index ad6594837d2d..04a402d620e7 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/ConstKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/ConstKeywordRecommenderTests.cs @@ -175,6 +175,14 @@ struct S { $$ """); + [Fact] + public Task TestInsideUnion() + => VerifyKeywordAsync( + """ + union U(int) { + $$ + """, CSharpNextParseOptions, CSharpNextScriptParseOptions); + [Fact] public Task TestInsideInterface() => VerifyKeywordAsync(""" diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/ProtectedKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/ProtectedKeywordRecommenderTests.cs index 8d7dcba6cdcf..0796c220a8fd 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/ProtectedKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/ProtectedKeywordRecommenderTests.cs @@ -181,6 +181,13 @@ struct S { $$ """); + [Fact] + public Task TestNotInsideUnion() + => VerifyAbsenceAsync(""" + union U(int) { + $$ + """, CSharpNextParseOptions, CSharpNextScriptParseOptions); + [Fact] public Task TestInsideInterface() => VerifyKeywordAsync(""" diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/PublicKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/PublicKeywordRecommenderTests.cs index 3f95c956756f..dcc1d7143519 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/PublicKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/PublicKeywordRecommenderTests.cs @@ -210,6 +210,14 @@ struct S { $$ """); + [Fact] + public Task TestInsideUnion() + => VerifyKeywordAsync( + """ + union U(int) { + $$ + """, CSharpNextParseOptions, CSharpNextScriptParseOptions); + [Fact] public Task TestInsideInterface() => VerifyKeywordAsync( diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/RecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/RecommenderTests.cs index 509e49df8d54..cf5953752f58 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/RecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/RecommenderTests.cs @@ -24,14 +24,20 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Recommendations; public abstract class RecommenderTests : TestBase { protected static readonly CSharpParseOptions CSharp9ParseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp9); - protected static readonly CSharpParseOptions CSharpNextParseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp14); - protected static readonly CSharpParseOptions CSharpNextScriptParseOptions = Options.Script.WithLanguageVersion(LanguageVersion.CSharp14); + protected static readonly CSharpParseOptions CSharp14ParseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp14); + protected static readonly CSharpParseOptions CSharpNextParseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersionExtensions.CSharpNext); + protected static readonly CSharpParseOptions CSharpNextScriptParseOptions = Options.Script.WithLanguageVersion(LanguageVersionExtensions.CSharpNext); + + // If no language version is specified, then we want to test against the implicit language version (ie. preview) + protected static readonly CSharpParseOptions CSharpImplicitParseOptions = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview); + protected static readonly CSharpParseOptions CSharpImplicitScriptParseOptions = Options.Script.WithLanguageVersion(LanguageVersion.Preview); protected abstract string KeywordText { get; } internal Func>>? RecommendKeywordsAsync; internal async Task VerifyWorkerAsync(string markup, bool absent, CSharpParseOptions? options = null, int? matchPriority = null) { + options ??= CSharpImplicitParseOptions; Testing.TestFileMarkupParser.GetPosition(markup, out var code, out var position); await VerifyAtPositionAsync(code, position, absent, options: options, matchPriority: matchPriority); await VerifyInFrontOfCommentAsync(code, position, absent, options: options, matchPriority: matchPriority); @@ -168,8 +174,10 @@ internal async Task VerifyKeywordAsync( CSharpParseOptions? scriptOptions = null) { // run the verification in both context(normal and script) + options ??= CSharpImplicitParseOptions; + scriptOptions ??= CSharpImplicitScriptParseOptions; await VerifyWorkerAsync(text, absent: false, options: options); - await VerifyWorkerAsync(text, absent: false, options: scriptOptions ?? Options.Script); + await VerifyWorkerAsync(text, absent: false, options: scriptOptions); } protected async Task VerifyKeywordAsync( @@ -183,7 +191,7 @@ protected async Task VerifyKeywordAsync( break; case SourceCodeKind.Script: - await VerifyWorkerAsync(text, absent: false, options: Options.Script); + await VerifyWorkerAsync(text, absent: false, options: CSharpImplicitScriptParseOptions); break; } } @@ -194,8 +202,10 @@ protected async Task VerifyAbsenceAsync( CSharpParseOptions? scriptOptions = null) { // run the verification in both context(normal and script) + options ??= CSharpImplicitParseOptions; + scriptOptions ??= CSharpImplicitScriptParseOptions; await VerifyWorkerAsync(text, absent: true, options: options); - await VerifyWorkerAsync(text, absent: true, options: scriptOptions ?? Options.Script); + await VerifyWorkerAsync(text, absent: true, options: scriptOptions); } protected async Task VerifyAbsenceAsync( @@ -208,7 +218,7 @@ protected async Task VerifyAbsenceAsync( await VerifyWorkerAsync(text, absent: true); break; case SourceCodeKind.Script: - await VerifyWorkerAsync(text, absent: true, options: Options.Script); + await VerifyWorkerAsync(text, absent: true, options: CSharpImplicitScriptParseOptions); break; } } diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/StaticKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/StaticKeywordRecommenderTests.cs index dcc4a3644898..609849364c9d 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/StaticKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/StaticKeywordRecommenderTests.cs @@ -229,6 +229,14 @@ struct S { $$ """); + [Fact] + public Task TestInsideUnion() + => VerifyKeywordAsync( + """ + union U(int) { + $$ + """, CSharpNextParseOptions, CSharpNextScriptParseOptions); + [Fact] public Task TestInsideInterface() => VerifyKeywordAsync(""" diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/ThisKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/ThisKeywordRecommenderTests.cs index 781907d9b5c3..d749d975369a 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/ThisKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/ThisKeywordRecommenderTests.cs @@ -1217,9 +1217,11 @@ public Task TestAfterRefExpression() => VerifyKeywordAsync(AddInsideMethod( @"ref int x = ref $$")); - [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/78979")] + [Theory, CombinatorialData] + [WorkItem("https://github.com/dotnet/roslyn/issues/78979")] + [WorkItem("https://github.com/dotnet/roslyn/issues/82251")] public Task TestInsideNameofInAttribute(bool isStatic) - => VerifyKeywordAsync($$""" + => VerifyAbsenceAsync($$""" public class Example { private string _field; diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/UnionKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/UnionKeywordRecommenderTests.cs new file mode 100644 index 000000000000..b373303183eb --- /dev/null +++ b/src/EditorFeatures/CSharpTest2/Recommendations/UnionKeywordRecommenderTests.cs @@ -0,0 +1,282 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Recommendations; + +[Trait(Traits.Feature, Traits.Features.KeywordRecommending)] +public sealed class UnionKeywordRecommenderTests : KeywordRecommenderTests +{ + [Fact] + public Task TestAfterGlobalStatement() + => VerifyKeywordAsync( + """ + System.Console.WriteLine(); + $$ + """); + + [Fact] + public Task TestNotInUsingAlias() + => VerifyAbsenceAsync( +@"using Goo = $$"); + + [Fact] + public Task TestNotInGlobalUsingAlias() + => VerifyAbsenceAsync("global using Goo = $$"); + + [Fact] + public Task TestNotInEmptyStatement() + => VerifyAbsenceAsync(AddInsideMethod("$$")); + + [Fact] + public Task TestInCompilationUnit() + => VerifyKeywordAsync("$$"); + + [Fact] + public Task TestInCompilationUnit_LangVer14() + => VerifyKeywordAsync("$$", CSharp14ParseOptions); + + [Fact] + public Task TestAfterExtern() + => VerifyKeywordAsync( + """ + extern alias Goo; + $$ + """); + + [Fact] + public Task TestAfterUsing() + => VerifyKeywordAsync( + """ + using Goo; + $$ + """); + + [Fact] + public Task TestAfterGlobalUsing() + => VerifyKeywordAsync( + """ + global using Goo; + $$ + """); + + [Fact] + public Task TestAfterNamespace() + => VerifyKeywordAsync( + """ + namespace N {} + $$ + """); + + [Fact] + public Task TestAfterFileScopedNamespace() + => VerifyKeywordAsync( + """ + namespace N; + $$ + """); + + [Fact] + public Task TestAfterTypeDeclaration() + => VerifyKeywordAsync( + """ + class C {} + $$ + """); + + [Fact] + public Task TestAfterDelegateDeclaration() + => VerifyKeywordAsync( + """ + delegate void Goo(); + $$ + """); + + [Fact] + public Task TestAfterMethod() + => VerifyKeywordAsync( + """ + class C { + void Goo() {} + $$ + """); + + [Fact] + public Task TestAfterField() + => VerifyKeywordAsync( + """ + class C { + int i; + $$ + """); + + [Fact] + public Task TestAfterProperty() + => VerifyKeywordAsync( + """ + class C { + int i { get; } + $$ + """); + + [Fact] + public Task TestNotBeforeUsing() + => VerifyAbsenceAsync(SourceCodeKind.Regular, + """ + $$ + using Goo; + """); + + [Fact] + public Task TestNotBeforeGlobalUsing() + => VerifyAbsenceAsync( + """ + $$ + global using Goo; + """); + + [Fact] + public Task TestAfterReadonly() + => VerifyKeywordAsync("readonly $$"); + + [Fact] + public Task TestNotAfterRef() + => VerifyAbsenceAsync("ref $$"); + + [Fact] + public Task TestNotAfterRefReadonly() + => VerifyAbsenceAsync("ref readonly $$"); + + [Fact] + public Task TestNotAfterPublicRefReadonly() + => VerifyAbsenceAsync("public ref readonly $$"); + + [Fact] + public Task TestNotAfterReadonlyRef() + => VerifyAbsenceAsync("readonly ref $$"); + + [Fact] + public Task TestNotAfterInternalReadonlyRef() + => VerifyAbsenceAsync("internal readonly ref $$"); + + [Fact] + public Task TestNotAfterReadonlyInMethod() + => VerifyAbsenceAsync("class C { void M() { readonly $$ } }"); + + [Fact] + public Task TestNotAfterRefInMethod() + => VerifyAbsenceAsync("class C { void M() { ref $$ } }"); + + [Fact] + public Task TestAfterAssemblyAttribute() + => VerifyKeywordAsync( + """ + [assembly: goo] + $$ + """); + + [Fact] + public Task TestAfterRootAttribute() + => VerifyKeywordAsync( + """ + [goo] + $$ + """); + + [Fact] + public Task TestAfterNestedAttribute() + => VerifyKeywordAsync( + """ + class C { + [goo] + $$ + """); + + [Fact] + public Task TestInsideStruct() + => VerifyKeywordAsync( + """ + struct S { + $$ + """); + + [Fact] + public Task TestInsideInterface() + => VerifyKeywordAsync(""" + interface I { + $$ + """); + + [Fact] + public Task TestInsideClass() + => VerifyKeywordAsync( + """ + class C { + $$ + """); + + [Fact] + public Task TestAfterPartial() + => VerifyKeywordAsync("partial $$"); + + [Fact] + public Task TestNotAfterAbstract() + => VerifyAbsenceAsync("abstract $$"); + + [Fact] + public Task TestAfterInternal() + => VerifyKeywordAsync("internal $$"); + + [Fact] + public Task TestAfterPublic() + => VerifyKeywordAsync("public $$"); + + [Fact] + public Task TestAfterFile() + => VerifyKeywordAsync(SourceCodeKind.Regular, "file $$"); + + [Fact] + public Task TestAfterPrivate() + => VerifyKeywordAsync("private $$"); + + [Fact] + public Task TestAfterProtected() + => VerifyKeywordAsync("protected $$"); + + [Fact] + public Task TestNotAfterSealed() + => VerifyAbsenceAsync("sealed $$"); + + [Fact] + public Task TestNotAfterStatic() + => VerifyAbsenceAsync("static $$"); + + [Fact] + public Task TestNotAfterAbstractPublic() + => VerifyAbsenceAsync("abstract public $$"); + + [Fact] + public Task TestNotAfterStruct() + => VerifyAbsenceAsync("struct $$"); + + [Fact] + public Task TestNotInTypeParameterConstraint() + => VerifyAbsenceAsync("class C where T : $$"); + + [Fact] + public Task TestWithinExtension() + => VerifyKeywordAsync( + """ + static class C + { + extension(string s) + { + $$ + } + } + """); +} diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/VirtualKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/VirtualKeywordRecommenderTests.cs index 5a157821f317..e32e3e0e1efa 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/VirtualKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/VirtualKeywordRecommenderTests.cs @@ -174,6 +174,13 @@ struct S { $$ """); + [Fact] + public Task TestNotInsideUnion() + => VerifyAbsenceAsync(""" + union U(int) { + $$ + """, CSharpNextParseOptions, CSharpNextScriptParseOptions); + [Fact] public Task TestInsideInterface() => VerifyKeywordAsync(""" diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs index 8c9db924ad65..058a43080656 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs @@ -216,6 +216,14 @@ struct S { $$ """); + [Fact] + public Task TestInsideUnion() + => VerifyKeywordAsync( + """ + union U(int) { + $$ + """, CSharpNextParseOptions, CSharpNextScriptParseOptions); + [Fact] public Task TestInsideInterface() => VerifyKeywordAsync( diff --git a/src/EditorFeatures/Core/Copilot/RoslynProposalAdjusterProvider.cs b/src/EditorFeatures/Core/Copilot/RoslynProposalAdjusterProvider.cs index 933b0e6de0bb..90ac2aaeb2a7 100644 --- a/src/EditorFeatures/Core/Copilot/RoslynProposalAdjusterProvider.cs +++ b/src/EditorFeatures/Core/Copilot/RoslynProposalAdjusterProvider.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Options; @@ -33,11 +34,13 @@ namespace Microsoft.CodeAnalysis.Copilot; internal sealed class RoslynProposalAdjusterProvider : ProposalAdjusterProviderBase { private readonly ImmutableHashSet _allowableAdjustments; + private readonly EditorOptionsService _editorOptionsService; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RoslynProposalAdjusterProvider(IGlobalOptionService globalOptions) + public RoslynProposalAdjusterProvider(IGlobalOptionService globalOptions, EditorOptionsService editorOptionsService) { + _editorOptionsService = editorOptionsService; var builder = ImmutableHashSet.CreateBuilder(); if (globalOptions.GetOption(CopilotOptions.FixAddMissingTokens)) builder.Add(ProposalAdjusterKinds.AddMissingTokens); @@ -202,12 +205,15 @@ bool ReportFailureTelemetry(Exception ex) // Checked in TryGetAffectedSolution Contract.ThrowIfNull(document); + var lineFormattingOptions = snapshot.TextBuffer.GetLineFormattingOptions(_editorOptionsService, explicitFormat: false); + var proposalAdjusterService = document.GetLanguageService(); var (proposedEdits, formatGroup, adjustmentResults) = proposalAdjusterService is null ? default : await proposalAdjusterService.TryAdjustProposalAsync( this._allowableAdjustments, document, - CopilotEditorUtilities.TryGetNormalizedTextChanges(editGroup), cancellationToken).ConfigureAwait(false); + CopilotEditorUtilities.TryGetNormalizedTextChanges(editGroup), + lineFormattingOptions, cancellationToken).ConfigureAwait(false); if (proposedEdits.IsDefault || adjustmentResults.IsDefault) { diff --git a/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs b/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs index 872b044c6ed8..42cc6155d172 100644 --- a/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs +++ b/src/EditorFeatures/Core/EditAndContinue/Contracts/ContractWrappers.cs @@ -2,9 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using Microsoft.VisualStudio.Debugger.Contracts.EditAndContinue; using Microsoft.VisualStudio.Debugger.Contracts.HotReload; using InternalContracts = Microsoft.CodeAnalysis.Contracts.EditAndContinue; diff --git a/src/EditorFeatures/Core/EditAndContinue/Contracts/ManagedHotReloadServiceBridge.cs b/src/EditorFeatures/Core/EditAndContinue/Contracts/ManagedHotReloadServiceBridge.cs deleted file mode 100644 index 5e956b35a8fe..000000000000 --- a/src/EditorFeatures/Core/EditAndContinue/Contracts/ManagedHotReloadServiceBridge.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Debugger.Contracts.HotReload; -using InternalContracts = Microsoft.CodeAnalysis.Contracts.EditAndContinue; - -namespace Microsoft.CodeAnalysis.EditAndContinue; - -internal sealed class ManagedHotReloadServiceBridge(IManagedHotReloadService service) : InternalContracts.IManagedHotReloadService -{ - public async ValueTask> GetActiveStatementsAsync(CancellationToken cancellation) - => (await service.GetActiveStatementsAsync(cancellation).ConfigureAwait(false)).SelectAsArray(a => a.ToContract()); - - public async ValueTask GetAvailabilityAsync(Guid module, CancellationToken cancellation) - => (await service.GetAvailabilityAsync(module, cancellation).ConfigureAwait(false)).ToContract(); - - public ValueTask> GetCapabilitiesAsync(CancellationToken cancellation) - => service.GetCapabilitiesAsync(cancellation); - - public ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellation) - => service.PrepareModuleForUpdateAsync(module, cancellation); -} diff --git a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageServiceBridge.cs b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageServiceBridge.cs deleted file mode 100644 index 80b8038ef7e9..000000000000 --- a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageServiceBridge.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.VisualStudio.Debugger.Contracts.HotReload; - -namespace Microsoft.CodeAnalysis.EditAndContinue; - -/// -/// Exposes as a brokered service. -/// TODO (https://github.com/dotnet/roslyn/issues/72713): -/// Once debugger is updated to use the brokered service, this class should be removed and should be exported directly. -/// -internal sealed partial class ManagedEditAndContinueLanguageServiceBridge(EditAndContinueLanguageService service) : IManagedHotReloadLanguageService3 -{ - public ValueTask StartSessionAsync(CancellationToken cancellationToken) - => service.StartSessionAsync(cancellationToken); - - public ValueTask EndSessionAsync(CancellationToken cancellationToken) - => service.EndSessionAsync(cancellationToken); - - public ValueTask EnterBreakStateAsync(CancellationToken cancellationToken) - => service.EnterBreakStateAsync(cancellationToken); - - public ValueTask ExitBreakStateAsync(CancellationToken cancellationToken) - => service.ExitBreakStateAsync(cancellationToken); - - public ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellationToken) - => service.OnCapabilitiesChangedAsync(cancellationToken); - - [Obsolete] - public ValueTask GetUpdatesAsync(CancellationToken cancellationToken) - => throw new NotImplementedException(); - - [Obsolete] - public ValueTask GetUpdatesAsync(ImmutableArray runningProjects, CancellationToken cancellationToken) - { - // StreamJsonRpc may use this overload when the method is invoked with empty parameters. Call the new implementation instead. - - if (!runningProjects.IsEmpty) - throw new NotImplementedException(); - - return GetUpdatesAsync(ImmutableArray.Empty, cancellationToken); - } - - public ValueTask GetUpdatesAsync(ImmutableArray runningProjects, CancellationToken cancellationToken) - => service.GetUpdatesAsync(runningProjects, cancellationToken); - - public ValueTask CommitUpdatesAsync(CancellationToken cancellationToken) - => service.CommitUpdatesAsync(cancellationToken); - - [Obsolete] - public ValueTask UpdateBaselinesAsync(ImmutableArray projectPaths, CancellationToken cancellationToken) - => throw new NotImplementedException(); - - public ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken) - => service.DiscardUpdatesAsync(cancellationToken); - - public ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) - => service.HasChangesAsync(sourceFilePath, cancellationToken); -} - diff --git a/src/EditorFeatures/Core/EditAndContinue/EditorActiveStatementTrackingController.cs b/src/EditorFeatures/Core/EditAndContinue/EditorActiveStatementTrackingController.cs new file mode 100644 index 000000000000..d95ae1f21132 --- /dev/null +++ b/src/EditorFeatures/Core/EditAndContinue/EditorActiveStatementTrackingController.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[Shared] +[Export(typeof(IActiveStatementTrackingController))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class EditorActiveStatementTrackingController(Lazy workspaceProvider) : IActiveStatementTrackingController +{ + private readonly IActiveStatementTrackingService _service + = workspaceProvider.Value.Workspace.Services.SolutionServices.GetRequiredService(); + + public void StartTracking(Solution solution, IActiveStatementSpanFactory spanProvider) + => _service.StartTracking(solution, spanProvider); + + public ActiveStatementSpanProvider GetSpanProvider(Solution solution) + => new((documentId, filePath, cancellationToken) => _service.GetSpansAsync(solution, documentId, filePath, cancellationToken)); + + public void EndTracking() + => _service.EndTracking(); +} diff --git a/src/EditorFeatures/Core/EditAndContinue/EditorHostSolutionProvider.cs b/src/EditorFeatures/Core/EditAndContinue/EditorHostSolutionProvider.cs new file mode 100644 index 000000000000..38b182a810a0 --- /dev/null +++ b/src/EditorFeatures/Core/EditAndContinue/EditorHostSolutionProvider.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[Shared] +[Export(typeof(ISolutionSnapshotProvider))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class EditorHostSolutionProvider(Lazy workspaceProvider) : ISolutionSnapshotProvider +{ + public ValueTask GetCurrentSolutionAsync(CancellationToken cancellationToken) + => ValueTask.FromResult(workspaceProvider.Value.Workspace.CurrentSolution); +} diff --git a/src/EditorFeatures/Core/EditAndContinue/IActiveStatementTrackingService.cs b/src/EditorFeatures/Core/EditAndContinue/IActiveStatementTrackingService.cs index 75dca605d4b4..f348f06e30b6 100644 --- a/src/EditorFeatures/Core/EditAndContinue/IActiveStatementTrackingService.cs +++ b/src/EditorFeatures/Core/EditAndContinue/IActiveStatementTrackingService.cs @@ -6,12 +6,11 @@ using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host; using Microsoft.VisualStudio.Text; namespace Microsoft.CodeAnalysis.EditAndContinue; -internal interface IActiveStatementTrackingService : IWorkspaceService, IActiveStatementSpanLocator +internal interface IActiveStatementTrackingService : IActiveStatementSpanLocator { void StartTracking(Solution solution, IActiveStatementSpanFactory spanProvider); diff --git a/src/VisualStudio/DevKit/Impl/EditAndContinue/ManagedHotReloadLanguageServiceBridge.cs b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs similarity index 75% rename from src/VisualStudio/DevKit/Impl/EditAndContinue/ManagedHotReloadLanguageServiceBridge.cs rename to src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs index 01a997e027fb..0fd88a62d64a 100644 --- a/src/VisualStudio/DevKit/Impl/EditAndContinue/ManagedHotReloadLanguageServiceBridge.cs +++ b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageService.cs @@ -15,10 +15,13 @@ namespace Microsoft.CodeAnalysis.EditAndContinue; +/// +/// Wrapper of implementing closed-source debugger contract interfaces. +/// [ExportBrokeredService(ManagedHotReloadLanguageServiceDescriptor.MonikerName, ManagedHotReloadLanguageServiceDescriptor.ServiceVersion, Audience = ServiceAudience.Local)] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed partial class ManagedHotReloadLanguageServiceBridge(InternalContracts.IManagedHotReloadLanguageService3 service) : IManagedHotReloadLanguageService3, IExportedBrokeredService +internal sealed class ManagedHotReloadLanguageService(ManagedHotReloadLanguageServiceImpl impl) : IManagedHotReloadLanguageService3, IExportedBrokeredService { ServiceRpcDescriptor IExportedBrokeredService.Descriptor => ManagedHotReloadLanguageServiceDescriptor.Descriptor; @@ -27,19 +30,19 @@ public Task InitializeAsync(CancellationToken cancellationToken) => Task.CompletedTask; public ValueTask StartSessionAsync(CancellationToken cancellationToken) - => service.StartSessionAsync(cancellationToken); + => impl.StartSessionAsync(cancellationToken); public ValueTask EndSessionAsync(CancellationToken cancellationToken) - => service.EndSessionAsync(cancellationToken); + => impl.EndSessionAsync(cancellationToken); public ValueTask EnterBreakStateAsync(CancellationToken cancellationToken) - => service.EnterBreakStateAsync(cancellationToken); + => impl.EnterBreakStateAsync(cancellationToken); public ValueTask ExitBreakStateAsync(CancellationToken cancellationToken) - => service.ExitBreakStateAsync(cancellationToken); + => impl.ExitBreakStateAsync(cancellationToken); public ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellationToken) - => service.OnCapabilitiesChangedAsync(cancellationToken); + => impl.OnCapabilitiesChangedAsync(cancellationToken); [Obsolete] public ValueTask GetUpdatesAsync(CancellationToken cancellationToken) @@ -56,18 +59,18 @@ public ValueTask GetUpdatesAsync(ImmutableArray } public async ValueTask GetUpdatesAsync(ImmutableArray runningProjects, CancellationToken cancellationToken) - => (await service.GetUpdatesAsync(runningProjects.SelectAsArray(static info => info.ToContract()), cancellationToken).ConfigureAwait(false)).FromContract(); + => (await impl.GetUpdatesAsync(runningProjects.SelectAsArray(static info => info.ToContract()), cancellationToken).ConfigureAwait(false)).FromContract(); public ValueTask CommitUpdatesAsync(CancellationToken cancellationToken) - => service.CommitUpdatesAsync(cancellationToken); + => impl.CommitUpdatesAsync(cancellationToken); [Obsolete] public ValueTask UpdateBaselinesAsync(ImmutableArray projectPaths, CancellationToken cancellationToken) => throw new NotImplementedException(); public ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken) - => service.DiscardUpdatesAsync(cancellationToken); + => impl.DiscardUpdatesAsync(cancellationToken); public ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) - => service.HasChangesAsync(sourceFilePath, cancellationToken); + => impl.HasChangesAsync(sourceFilePath, cancellationToken); } diff --git a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs similarity index 83% rename from src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs rename to src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs index 225ab405f36d..889104a86ef3 100644 --- a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs +++ b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadLanguageServiceImpl.cs @@ -6,32 +6,34 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; -using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Contracts.EditAndContinue; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.VisualStudio.Debugger.Contracts.HotReload; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.EditAndContinue; +/// +/// Implementation of a brokered service available in Visual Studio in-proc container and in DevKit. +/// [Shared] -[Export(typeof(IManagedHotReloadLanguageService3))] [Export(typeof(IEditAndContinueSolutionProvider))] -[Export(typeof(EditAndContinueLanguageService))] -[ExportMetadata("UIContext", EditAndContinueUIContext.EncCapableProjectExistsInWorkspaceUIContextString)] +[Export(typeof(ManagedHotReloadLanguageServiceImpl))] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class EditAndContinueLanguageService( +internal sealed class ManagedHotReloadLanguageServiceImpl( EditAndContinueSessionState sessionState, Lazy workspaceProvider, - Lazy debuggerService, + IManagedHotReloadService debuggerService, + ISolutionSnapshotProvider solutionSnapshotProvider, PdbMatchingSourceTextProvider sourceTextProvider, + IActiveStatementTrackingController activeStatementTrackingController, IEditAndContinueLogReporter logReporter, IDiagnosticsRefresher diagnosticRefresher) : IManagedHotReloadLanguageService3, IEditAndContinueSolutionProvider { @@ -46,41 +48,16 @@ public NoSessionException() } private bool _disabled; - private RemoteDebuggingSessionProxy? _debuggingSession; + private DebuggingSessionProxy? _debuggingSession; private Solution? _pendingUpdatedSolution; private Solution? _committedSolution; public event Action? SolutionCommitted; - public void SetFileLoggingDirectory(string? logDirectory) - { - _ = Task.Run(async () => - { - try - { - var proxy = new RemoteEditAndContinueServiceProxy(Services); - await proxy.SetFileLoggingDirectoryAsync(logDirectory, CancellationToken.None).ConfigureAwait(false); - } - catch - { - // ignore - } - }); - } - - private SolutionServices Services - => workspaceProvider.Value.Workspace.Services.SolutionServices; - - private Solution GetCurrentSolution() - => workspaceProvider.Value.Workspace.CurrentSolution; - - private RemoteDebuggingSessionProxy GetDebuggingSession() + private DebuggingSessionProxy GetDebuggingSession() => _debuggingSession ?? throw new NoSessionException(); - private IActiveStatementTrackingService GetActiveStatementTrackingService() - => Services.GetRequiredService(); - internal void Disable(Exception e) { _disabled = true; @@ -111,16 +88,16 @@ public async ValueTask StartSessionAsync(CancellationToken cancellationToken) // so that we don't miss any pertinent workspace update events. sourceTextProvider.Activate(); - var currentSolution = GetCurrentSolution(); + var currentSolution = await solutionSnapshotProvider.GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false); _committedSolution = currentSolution; sourceTextProvider.SetBaseline(currentSolution); - var proxy = new RemoteEditAndContinueServiceProxy(Services); + var proxy = new RemoteEditAndContinueServiceProxy(currentSolution.Services); _debuggingSession = await proxy.StartDebuggingSessionAsync( currentSolution, - new ManagedHotReloadServiceBridge(debuggerService.Value), + debuggerService, sourceTextProvider, reportDiagnostics: true, cancellationToken).ConfigureAwait(false); @@ -151,13 +128,13 @@ private async ValueTask BreakStateOrCapabilitiesChangedAsync(bool? inBreakState, try { var session = GetDebuggingSession(); - var solution = (inBreakState == true) ? GetCurrentSolution() : null; + var solution = (inBreakState == true) ? await solutionSnapshotProvider.GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false) : null; await session.BreakStateOrCapabilitiesChangedAsync(inBreakState, cancellationToken).ConfigureAwait(false); if (inBreakState == false) { - GetActiveStatementTrackingService().EndTracking(); + activeStatementTrackingController.EndTracking(); } else if (inBreakState == true) { @@ -168,7 +145,7 @@ private async ValueTask BreakStateOrCapabilitiesChangedAsync(bool? inBreakState, // The tracking session is cancelled when we exit the break state. Contract.ThrowIfNull(solution); - GetActiveStatementTrackingService().StartTracking(solution, session); + activeStatementTrackingController.StartTracking(solution, session); } } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) @@ -258,12 +235,6 @@ public async ValueTask EndSessionAsync(CancellationToken cancellationToken) _pendingUpdatedSolution = null; } - private ActiveStatementSpanProvider GetActiveStatementSpanProvider(Solution solution) - { - var service = GetActiveStatementTrackingService(); - return new((documentId, filePath, cancellationToken) => service.GetSpansAsync(solution, documentId, filePath, cancellationToken)); - } - /// /// Returns true if any changes have been made to the source since the last changes had been applied. /// For performance reasons it only implements a heuristic and may return both false positives and false negatives. @@ -288,7 +259,7 @@ public async ValueTask HasChangesAsync(string? sourceFilePath, Cancellatio Contract.ThrowIfNull(_committedSolution); var oldSolution = _committedSolution; - var newSolution = GetCurrentSolution(); + var newSolution = await solutionSnapshotProvider.GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false); return (sourceFilePath != null) ? await EditSession.HasChangesAsync(oldSolution, newSolution, sourceFilePath, cancellationToken).ConfigureAwait(false) @@ -319,11 +290,11 @@ public async ValueTask GetUpdatesAsync(ImmutableArray (info.ProjectInstanceId.ProjectFilePath, info.ProjectInstanceId.TargetFramework, info.RestartAutomatically)); var result = await GetDebuggingSession().EmitSolutionUpdateAsync(solution, runningProjectOptions, activeStatementSpanProvider, cancellationToken).ConfigureAwait(false); @@ -364,8 +335,8 @@ await solution.GetDocumentAsync(diagnostic.DocumentId, includeSourceGenerated: t UpdateApplyChangesDiagnostics(applyChangesDiagnostics.ToImmutableOrEmptyAndFree()); return new ManagedHotReloadUpdates( - result.ModuleUpdates.Updates.FromContract(), - result.GetAllDiagnostics().FromContract(), + result.ModuleUpdates.Updates, + result.GetAllDiagnostics(), ToProjectIntanceIds(result.ProjectsToRebuild), ToProjectIntanceIds(result.ProjectsToRestart.Keys)); diff --git a/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadServiceProxy.cs b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadServiceProxy.cs new file mode 100644 index 000000000000..a4c4e66681bd --- /dev/null +++ b/src/EditorFeatures/Core/EditAndContinue/ManagedHotReloadServiceProxy.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.BrokeredServices; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.Debugger.Contracts.HotReload; +using InternalContracts = Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[Shared] +[Export(typeof(InternalContracts.IManagedHotReloadService))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ManagedHotReloadServiceProxy(IServiceBrokerProvider serviceBrokerProvider) : + BrokeredServiceProxy(serviceBrokerProvider.ServiceBroker, BrokeredServiceDescriptors.DebuggerManagedHotReloadService), + InternalContracts.IManagedHotReloadService +{ + public async ValueTask> GetActiveStatementsAsync(CancellationToken cancellationToken) + { + var result = await InvokeAsync((service, cancellationToken) => service.GetActiveStatementsAsync(cancellationToken), cancellationToken).ConfigureAwait(false); + return result.SelectAsArray(info => info.ToContract()); + } + + public async ValueTask GetAvailabilityAsync(Guid module, CancellationToken cancellationToken) + { + var result = await InvokeAsync((service, module, cancellationToken) => service.GetAvailabilityAsync(module, cancellationToken), module, cancellationToken).ConfigureAwait(false); + return result.ToContract(); + } + + public ValueTask> GetCapabilitiesAsync(CancellationToken cancellationToken) + => InvokeAsync((service, cancellationToken) => service.GetCapabilitiesAsync(cancellationToken), cancellationToken); + + public ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellationToken) + => InvokeAsync((service, module, cancellationToken) => service.PrepareModuleForUpdateAsync(module, cancellationToken), module, cancellationToken); +} diff --git a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs index c59fc4eb7daa..27214c8bc437 100644 --- a/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/LanguageServer/AbstractInProcLanguageClient.cs @@ -9,6 +9,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer; @@ -121,7 +122,21 @@ public Task StopServerAsync() { if (_languageServer is not null) { - await _languageServer.WaitForExitAsync().WithCancellation(cancellationToken).ConfigureAwait(false); + try + { + await _languageServer.WaitForExitAsync().WithCancellation(cancellationToken).ConfigureAwait(false); + } + catch (ServerNotShutDownException) + { + // The previous instance is running and has not been asked to shutdown. This indicates an error + // in the caller where a new server instance is being created before the previous is shutdown. + throw; + } + catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex)) + { + // The previous server instance encountered an unexpected error during its lifecycle. + // Since we're creating a new server instance to replace it, we report the error and proceed. + } } var (clientStream, serverStream) = FullDuplexStream.CreatePair(); diff --git a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj index 8394dfd64bce..671fde963e01 100644 --- a/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj +++ b/src/EditorFeatures/Core/Microsoft.CodeAnalysis.EditorFeatures.csproj @@ -63,7 +63,6 @@ - diff --git a/src/EditorFeatures/Core/ModernCommands/ShowInheritanceMarginCommandArgs.cs b/src/EditorFeatures/Core/ModernCommands/ShowInheritanceMarginCommandArgs.cs new file mode 100644 index 000000000000..77c1e9feb282 --- /dev/null +++ b/src/EditorFeatures/Core/ModernCommands/ShowInheritanceMarginCommandArgs.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.Commanding; + +namespace Microsoft.CodeAnalysis.Editor.Commanding.Commands; + +/// +/// Arguments for the Show Inheritance command being invoked. +/// +[ExcludeFromCodeCoverage] +internal sealed class ShowInheritanceMarginCommandArgs(ITextView textView, ITextBuffer subjectBuffer) + : EditorCommandArgs(textView, subjectBuffer); diff --git a/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs b/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs index 23054eeda6f9..ea16b031948d 100644 --- a/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs +++ b/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Preview; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -415,7 +416,7 @@ public Task> CreateRemovedAnalyzerCo // Convert the diffs to be line based. // Compute the diffs between the old text and the new. - var diffResult = ComputeEditDifferences(oldDocument, newDocument, cancellationToken); + var diffResult = ComputeEditDifferences(oldBuffer.CurrentSnapshot, newBuffer.CurrentSnapshot, oldDocument.Project.Services, cancellationToken); // Need to show the spans in the right that are different. // We also need to show the spans that are in conflict. @@ -504,7 +505,7 @@ public Task> CreateRemovedAnalyzerCo // Convert the diffs to be line based. // Compute the diffs between the old text and the new. - var diffResult = ComputeEditDifferences(oldDocument, newDocument, cancellationToken); + var diffResult = ComputeEditDifferences(oldBuffer.CurrentSnapshot, newBuffer.CurrentSnapshot, oldDocument.Project.Services, cancellationToken); // Need to show the spans in the right that are different. var originalSpans = GetOriginalSpans(diffResult, cancellationToken); @@ -742,21 +743,17 @@ private static void MergeLineSpans(List lineSpans, LineSpan nextLineSp lineSpans.Add(nextLineSpan); } - private IHierarchicalDifferenceCollection ComputeEditDifferences(TextDocument oldDocument, TextDocument newDocument, CancellationToken cancellationToken) + private IHierarchicalDifferenceCollection ComputeEditDifferences(ITextSnapshot oldSnapshot, ITextSnapshot newSnapshot, LanguageServices services, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - // Get the text that's actually in the editor. - var oldText = oldDocument.GetTextSynchronously(cancellationToken); - var newText = newDocument.GetTextSynchronously(cancellationToken); - // Defer to the editor to figure out what changes the client made. var diffService = _differenceSelectorService.GetTextDifferencingService( - oldDocument.Project.Services.GetRequiredService().GetDefaultContentType()); + services.GetRequiredService().GetDefaultContentType()); diffService ??= _differenceSelectorService.DefaultTextDifferencingService; - return diffService.DiffSourceTexts(oldText, newText, s_differenceOptions); + return diffService.DiffSnapshotSpans(oldSnapshot.GetFullSpan(), newSnapshot.GetFullSpan(), s_differenceOptions); } private static NormalizedSpanCollection GetOriginalSpans(IHierarchicalDifferenceCollection diffResult, CancellationToken cancellationToken) diff --git a/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.cs b/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.cs index b328be35f519..d7211a324d95 100644 --- a/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.cs +++ b/src/EditorFeatures/Core/Shared/Extensions/ITextViewExtensions.cs @@ -144,6 +144,9 @@ public static bool TryMoveCaretToAndEnsureVisible(this ITextView textView, Virtu outliningManager?.ExpandAll(new SnapshotSpan(pointInView.Value, length: 0), match: _ => true); } + // Moving caret doesn't clear previous selection so we need to clear it manually + textView.Selection.Clear(); + var newPosition = textView.Caret.MoveTo(new VirtualSnapshotPoint(pointInView.Value, point.VirtualSpaces)); // We use the caret's position in the view's current snapshot here in case something diff --git a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs index 5ca068091ef5..9be8ade708bb 100644 --- a/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs +++ b/src/EditorFeatures/Core/Shared/Tagging/Utilities/TagSpanIntervalTree.cs @@ -215,7 +215,7 @@ void AddTagsForLargeNumberOfSpans(NormalizedSnapshotSpanCollection requestedSpan continue; } - // The current tag is *after* teh current span we're trying to intersect with. Move to the next span to + // The current tag is *after* the current span we're trying to intersect with. Move to the next span to // see if it intersects with the current tag. if (currentTagSpan.Start > currentRequestSpan.End) { diff --git a/src/EditorFeatures/Core/SolutionEvents/HostLegacySolutionEventsWorkspaceEventListener.cs b/src/EditorFeatures/Core/SolutionEvents/HostLegacySolutionEventsWorkspaceEventListener.cs index 2118302a39f9..ff346f7689f9 100644 --- a/src/EditorFeatures/Core/SolutionEvents/HostLegacySolutionEventsWorkspaceEventListener.cs +++ b/src/EditorFeatures/Core/SolutionEvents/HostLegacySolutionEventsWorkspaceEventListener.cs @@ -34,6 +34,8 @@ internal sealed partial class HostLegacySolutionEventsWorkspaceEventListener : I private WorkspaceEventRegistration? _workspaceChangedDisposer; + private bool? _processSourceGeneratedDocuments; + [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public HostLegacySolutionEventsWorkspaceEventListener( @@ -55,7 +57,12 @@ public void StartListening(Workspace workspace) // We only support this option to disable crawling in internal speedometer and ddrit perf runs to lower noise. // It is not exposed to the user. if (_globalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) + { + // Fetch whether we're processing source-generated files or not. We latch whatever we first read, to avoid any cases where changing the option might cause + // inconsistent analysis. + _processSourceGeneratedDocuments ??= _globalOptions.GetOption(SolutionCrawlerRegistrationService.ProcessRoslynSourceGeneratedFiles); _workspaceChangedDisposer = workspace.RegisterWorkspaceChangedHandler(OnWorkspaceChanged); + } } public void StopListening(Workspace workspace) @@ -93,6 +100,9 @@ private async ValueTask ProcessWorkspaceChangeEventsAsync(ImmutableSegmentedList var workspace = events[0].OldSolution.Workspace; Contract.ThrowIfTrue(events.Any(e => e.OldSolution.Workspace != workspace || e.NewSolution.Workspace != workspace)); + // We should have initialized this before subscribing to the workspace changed events + Contract.ThrowIfFalse(_processSourceGeneratedDocuments.HasValue); + var client = await RemoteHostClient.TryGetClientAsync(workspace, cancellationToken).ConfigureAwait(false); if (client is null) @@ -103,7 +113,7 @@ private async ValueTask ProcessWorkspaceChangeEventsAsync(ImmutableSegmentedList return; foreach (var args in events) - await aggregationService.OnWorkspaceChangedAsync(args, cancellationToken).ConfigureAwait(false); + await aggregationService.OnWorkspaceChangedAsync(args, _processSourceGeneratedDocuments.Value, cancellationToken).ConfigureAwait(false); } else { @@ -123,7 +133,7 @@ private async ValueTask ProcessWorkspaceChangeEventsAsync(ImmutableSegmentedList await client.TryInvokeAsync( args.OldSolution, args.NewSolution, (service, oldSolutionChecksum, newSolutionChecksum, cancellationToken) => - service.OnWorkspaceChangedAsync(oldSolutionChecksum, newSolutionChecksum, args.Kind, args.ProjectId, args.DocumentId, cancellationToken), + service.OnWorkspaceChangedAsync(oldSolutionChecksum, newSolutionChecksum, args.Kind, args.ProjectId, args.DocumentId, _processSourceGeneratedDocuments.Value, cancellationToken), cancellationToken).ConfigureAwait(false); } } diff --git a/src/EditorFeatures/Core/TextDiffing/EditorTextDifferencingService.cs b/src/EditorFeatures/Core/TextDiffing/EditorTextDifferencingService.cs index 5ab529d2c8c3..40c80a2d4d67 100644 --- a/src/EditorFeatures/Core/TextDiffing/EditorTextDifferencingService.cs +++ b/src/EditorFeatures/Core/TextDiffing/EditorTextDifferencingService.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Immutable; using System.Composition; @@ -13,6 +11,7 @@ using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Differencing; namespace Microsoft.CodeAnalysis.Editor.Implementation.TextDiffing; @@ -20,9 +19,10 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.TextDiffing; [ExportWorkspaceService(typeof(IDocumentTextDifferencingService), ServiceLayer.Host), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class EditorTextDifferencingService(ITextDifferencingSelectorService differenceSelectorService) : IDocumentTextDifferencingService +internal sealed class EditorTextDifferencingService(ITextDifferencingSelectorService differenceSelectorService, ITextBufferFactoryService bufferFactoryService) : IDocumentTextDifferencingService { private readonly ITextDifferencingSelectorService _differenceSelectorService = differenceSelectorService; + private readonly ITextBufferFactoryService _bufferFactoryService = bufferFactoryService; public Task> GetTextChangesAsync(Document oldDocument, Document newDocument, CancellationToken cancellationToken) => GetTextChangesAsync(oldDocument, newDocument, TextDifferenceTypes.Word, cancellationToken); @@ -32,12 +32,12 @@ public async Task> GetTextChangesAsync(Document oldDo var oldText = await oldDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var diffService = _differenceSelectorService.GetTextDifferencingService(oldDocument.Project.Services.GetService().GetDefaultContentType()) + var diffService = _differenceSelectorService.GetTextDifferencingService(oldDocument.Project.Services.GetRequiredService().GetDefaultContentType()) ?? _differenceSelectorService.DefaultTextDifferencingService; var differenceOptions = GetDifferenceOptions(preferredDifferenceType); - var diffResult = diffService.DiffSourceTexts(oldText, newText, differenceOptions); + var diffResult = diffService.DiffSourceTexts(oldText, newText, _bufferFactoryService, differenceOptions); return [.. diffResult.Differences.Select(d => new TextChange( diff --git a/src/EditorFeatures/Core/TextDiffing/TextDifferencingServiceExtensions.cs b/src/EditorFeatures/Core/TextDiffing/TextDifferencingServiceExtensions.cs index de20a96821b2..42ded9df6d06 100644 --- a/src/EditorFeatures/Core/TextDiffing/TextDifferencingServiceExtensions.cs +++ b/src/EditorFeatures/Core/TextDiffing/TextDifferencingServiceExtensions.cs @@ -3,23 +3,42 @@ // See the LICENSE file in the project root for more information. using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Text.Implementation; using Microsoft.CodeAnalysis.Text.Shared.Extensions; +using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Utilities; namespace Microsoft.CodeAnalysis.Editor.Implementation.TextDiffing; internal static class TextDifferencingServiceExtensions { - public static IHierarchicalDifferenceCollection DiffSourceTexts(this ITextDifferencingService diffService, SourceText oldText, SourceText newText, StringDifferenceOptions options) + public static IHierarchicalDifferenceCollection DiffSourceTexts(this ITextDifferencingService diffService, SourceText oldText, SourceText newText, ITextBufferFactoryService bufferFactoryService, StringDifferenceOptions options) { var oldTextSnapshot = oldText.FindCorrespondingEditorTextSnapshot(); var newTextSnapshot = newText.FindCorrespondingEditorTextSnapshot(); - var useSnapshots = oldTextSnapshot != null && newTextSnapshot != null; - var diffResult = useSnapshots - ? diffService.DiffSnapshotSpans(oldTextSnapshot!.GetFullSpan(), newTextSnapshot!.GetFullSpan(), options) - : diffService.DiffStrings(oldText.ToString(), newText.ToString(), options); + if (oldTextSnapshot != null || newTextSnapshot != null) + { + // If either source text has an associated snapshot, then we can utilize that + // snapshot to compute the diff. This allows us to avoid allocating strings + // for the diffing process, which can be expensive for large files. + oldTextSnapshot ??= CreateTextSnapshot(oldText, bufferFactoryService, newTextSnapshot!.ContentType); + newTextSnapshot ??= CreateTextSnapshot(newText, bufferFactoryService, oldTextSnapshot!.ContentType); - return diffResult; + return diffService.DiffSnapshotSpans(oldTextSnapshot.GetFullSpan(), newTextSnapshot.GetFullSpan(), options); + + static ITextSnapshot CreateTextSnapshot(SourceText text, ITextBufferFactoryService bufferFactoryService, IContentType contentType) + { + // Unable to find an existing snapshot for the given SourceText. Create a temporary one to aid in diff computation. + var reader = new SourceTextReader(text); + var buffer = bufferFactoryService.CreateTextBuffer(reader, contentType); + + return buffer.CurrentSnapshot; + } + } + + // Fallback to diffing by string + return diffService.DiffStrings(oldText.ToString(), newText.ToString(), options); } } diff --git a/src/EditorFeatures/ExternalAccess/Debugger/GlassTestsHotReloadService.cs b/src/EditorFeatures/ExternalAccess/Debugger/GlassTestsHotReloadService.cs index 79547aee6495..adc671745961 100644 --- a/src/EditorFeatures/ExternalAccess/Debugger/GlassTestsHotReloadService.cs +++ b/src/EditorFeatures/ExternalAccess/Debugger/GlassTestsHotReloadService.cs @@ -2,39 +2,46 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.Host; using Microsoft.VisualStudio.Debugger.Contracts.HotReload; -using Roslyn.Utilities; + +using InternalContracts = Microsoft.CodeAnalysis.Contracts.EditAndContinue; namespace Microsoft.CodeAnalysis.ExternalAccess.Debugger; -internal sealed class GlassTestsHotReloadService +internal sealed class GlassTestsHotReloadService(HostWorkspaceServices services, IManagedHotReloadService debuggerService) { - private static readonly ActiveStatementSpanProvider s_noActiveStatementSpanProvider = - async (_, _, _) => ImmutableArray.Empty; + internal sealed class ServiceWrapper(IManagedHotReloadService service) : InternalContracts.IManagedHotReloadService + { + public async ValueTask> GetActiveStatementsAsync(CancellationToken cancellation) + => (await service.GetActiveStatementsAsync(cancellation).ConfigureAwait(false)).SelectAsArray(a => a.ToContract()); - private readonly IManagedHotReloadService _debuggerService; + public async ValueTask GetAvailabilityAsync(Guid module, CancellationToken cancellation) + => (await service.GetAvailabilityAsync(module, cancellation).ConfigureAwait(false)).ToContract(); - private readonly IEditAndContinueService _encService; - private DebuggingSessionId _sessionId; + public ValueTask> GetCapabilitiesAsync(CancellationToken cancellation) + => service.GetCapabilitiesAsync(cancellation); - public GlassTestsHotReloadService(HostWorkspaceServices services, IManagedHotReloadService debuggerService) - { - _encService = services.GetRequiredService().Service; - _debuggerService = debuggerService; + public ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellation) + => service.PrepareModuleForUpdateAsync(module, cancellation); } + private static readonly ActiveStatementSpanProvider s_noActiveStatementSpanProvider = async (_, _, _) => []; + private readonly IEditAndContinueService _encService = services.GetRequiredService().Service; + private DebuggingSessionId _sessionId; + #pragma warning disable IDE0060 // Remove unused parameter public async Task StartSessionAsync(Solution solution, CancellationToken cancellationToken) #pragma warning restore IDE0060 { var newSessionId = _encService.StartDebuggingSession( solution, - new ManagedHotReloadServiceBridge(_debuggerService), + new ServiceWrapper(debuggerService), NullPdbMatchingSourceTextProvider.Instance, reportDiagnostics: false); diff --git a/src/EditorFeatures/Test/CodeFixes/ExtensionOrderingTests.cs b/src/EditorFeatures/Test/CodeFixes/ExtensionOrderingTests.cs index 20fd594ca941..e1785435267c 100644 --- a/src/EditorFeatures/Test/CodeFixes/ExtensionOrderingTests.cs +++ b/src/EditorFeatures/Test/CodeFixes/ExtensionOrderingTests.cs @@ -137,7 +137,7 @@ public void TestCodeFixServiceOrderIsCorrect(string language) var actualOrder = codeFixPriorityMap.OrderBy(kvp => kvp.Value).SelectAsArray(kvp => kvp.Key); - // Ok, now go through and ensure that all the items in teh CodeFixProvider are ordered as the + // Ok, now go through and ensure that all the items in the CodeFixProvider are ordered as the // ExtensionOrderer would order them. var currentIndex = expectedOrder.IndexOf(actualOrder[0]); diff --git a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueLanguageServiceTests.cs b/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs similarity index 95% rename from src/EditorFeatures/Test/EditAndContinue/EditAndContinueLanguageServiceTests.cs rename to src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs index 946c95d80e46..97774020ca5a 100644 --- a/src/EditorFeatures/Test/EditAndContinue/EditAndContinueLanguageServiceTests.cs +++ b/src/EditorFeatures/Test/EditAndContinue/EditorManagedHotReloadLanguageServiceTests.cs @@ -34,19 +34,19 @@ namespace Microsoft.CodeAnalysis.Editor.UnitTests.EditAndContinue; [UseExportProvider] -public sealed class EditAndContinueLanguageServiceTests : EditAndContinueWorkspaceTestBase +public sealed class EditorManagedHotReloadLanguageServiceTests : EditAndContinueWorkspaceTestBase { private static string Inspect(DiagnosticData d) => $"{d.Severity} {d.Id}:" + (!string.IsNullOrWhiteSpace(d.DataLocation.UnmappedFileSpan.Path) ? $" {d.DataLocation.UnmappedFileSpan.Path}({d.DataLocation.UnmappedFileSpan.StartLinePosition.Line}, {d.DataLocation.UnmappedFileSpan.StartLinePosition.Character}, {d.DataLocation.UnmappedFileSpan.EndLinePosition.Line}, {d.DataLocation.UnmappedFileSpan.EndLinePosition.Character}):" : "") + $" {d.Message}"; - private static string Inspect(DebuggerContracts.ManagedHotReloadDiagnostic d) + private static string Inspect(ManagedHotReloadDiagnostic d) => $"{d.Severity} {d.Id}:" + (!string.IsNullOrWhiteSpace(d.FilePath) ? $" {d.FilePath}({d.Span.StartLine}, {d.Span.StartColumn}, {d.Span.EndLine}, {d.Span.EndColumn}):" : "") + $" {d.Message}"; - private TestWorkspace CreateEditorWorkspace(out Solution solution, out EditAndContinueService service, out EditAndContinueLanguageService languageService, Type[] additionalParts = null) + private TestWorkspace CreateEditorWorkspace(out Solution solution, out EditAndContinueService service, out ManagedHotReloadLanguageServiceImpl languageService, Type[] additionalParts = null) { var composition = EditorTestCompositions.EditorFeatures .AddExcludedPartTypes(typeof(ServiceBrokerProvider)) @@ -72,21 +72,10 @@ private TestWorkspace CreateEditorWorkspace(out Solution solution, out EditAndCo solution = workspace.CurrentSolution; service = GetEditAndContinueService(workspace); - languageService = workspace.GetService(); + languageService = workspace.GetService(); return workspace; } - private sealed class TestSourceTextContainer : SourceTextContainer - { - public SourceText Text { get; set; } - - public override SourceText CurrentText => Text; - -#pragma warning disable CS0067 - public override event EventHandler TextChanged; -#pragma warning restore - } - [Theory, CombinatorialData] public async Task Test(bool commitChanges) { @@ -116,7 +105,7 @@ public async Task Test(bool commitChanges) mockEncService = (MockEditAndContinueService)localWorkspace.GetService(); - var localService = localWorkspace.GetService(); + var localService = localWorkspace.GetService(); await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution .AddTestProject("proj", out var projectId) @@ -216,11 +205,9 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution }; }; - var runningProjectInfo = new DebuggerContracts.RunningProjectInfo() - { - ProjectInstanceId = new DebuggerContracts.ProjectInstanceId(project.FilePath, "net10.0"), - RestartAutomatically = false, - }; + var runningProjectInfo = new RunningProjectInfo( + new ProjectInstanceId(project.FilePath, "net10.0"), + restartAutomatically: false); var updates = await localService.GetUpdatesAsync(runningProjects: [runningProjectInfo], CancellationToken.None); diff --git a/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.cs b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.cs index a28aec926349..7d59b1f830c1 100644 --- a/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.cs +++ b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameParserTests.cs @@ -420,7 +420,6 @@ public void TestFileInformation_InvalidDirectory() [InlineData(@"at M.1c()")] // Invalid start character for identifier [InlineData(@"at 1M.C()")] [InlineData(@"at M.C(string& s)")] // "string&" represents a reference (ref, out) and is not supported yet - [InlineData(@"at StreamJsonRpc.JsonRpc.d__139`1.MoveNext()")] // Generated/Inline methods are not supported yet [InlineData(@"at M(")] // Missing closing paren [InlineData(@"at M)")] // MIssing open paren [InlineData(@"at M.M[T>(T t)")] // Mismatched generic opening/close @@ -519,4 +518,18 @@ public void TestLanguages(string at, string @in, string line) line: CreateToken(StackFrameKind.NumberToken, "16", leadingTrivia: [CreateTrivia(StackFrameKind.LineTrivia, $"{line} ")]), inTrivia: CreateTrivia(StackFrameKind.InTrivia, $" {@in} ")) ); + + [Fact] + public void TestStateMachineMethod() + => Verify("Test.d__610.MoveNext()", + methodDeclaration: MethodDeclaration( + QualifiedName( + Identifier("Test"), + StateMachineMethod( + GeneratedName("MyAsyncMethod", endWithDollar: false), + suffix: "610", + stateMachineMethod: "MoveNext") + ), + argumentList: EmptyParams) + ); } diff --git a/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameSyntaxFactory.cs b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameSyntaxFactory.cs index 90c1c567b900..56faa1f4683c 100644 --- a/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameSyntaxFactory.cs +++ b/src/EditorFeatures/Test/EmbeddedLanguages/StackFrame/StackFrameSyntaxFactory.cs @@ -217,4 +217,11 @@ public static StackFrameLocalMethodNameNode LocalMethod(StackFrameGeneratedMetho IdentifierToken(identifier), PipeToken, CreateToken(StackFrameKind.GeneratedNameSuffixToken, suffix)); + + public static StackFrameStateMachineMethodNameNode StateMachineMethod(StackFrameGeneratedMethodNameNode encapsulatingMethod, string suffix, string stateMachineMethod) + => new( + encapsulatingMethod, + CreateToken(StackFrameKind.GeneratedNameSeparatorToken, "d__" + suffix), + DotToken, + IdentifierToken(stateMachineMethod)); } diff --git a/src/EditorFeatures/Test/InheritanceMargin/InheritanceMarginTests.cs b/src/EditorFeatures/Test/InheritanceMargin/InheritanceMarginTests.cs index f4650be4a5ac..5daa85030c66 100644 --- a/src/EditorFeatures/Test/InheritanceMargin/InheritanceMarginTests.cs +++ b/src/EditorFeatures/Test/InheritanceMargin/InheritanceMarginTests.cs @@ -167,6 +167,7 @@ private static async Task VerifyInheritanceTargetAsync(Workspace workspace, Test for (var i = 0; i < actualDocumentSpans.Length; i++) { var docSpan = await actualDocumentSpans[i].TryRehydrateAsync(workspace.CurrentSolution, CancellationToken.None); + Assert.NotNull(docSpan); Assert.Equal(expectedDocumentSpans[i].SourceSpan, docSpan.Value.SourceSpan); Assert.Equal(expectedDocumentSpans[i].Document.FilePath, docSpan.Value.Document.FilePath); } diff --git a/src/EditorFeatures/Test/Utilities/PatternMatcherTests.cs b/src/EditorFeatures/Test/Utilities/PatternMatcherTests.cs index 2936d807119f..3c0bce0f0953 100644 --- a/src/EditorFeatures/Test/Utilities/PatternMatcherTests.cs +++ b/src/EditorFeatures/Test/Utilities/PatternMatcherTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -13,7 +13,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.PatternMatching; -using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; @@ -422,14 +421,14 @@ public void TryMatchSingleWordPattern_CultureAwareSingleWordPreferCaseSensitiveE [Fact] public void TestCachingOfPriorResult() { - using var matcher = PatternMatcher.CreatePatternMatcher("Goo", includeMatchedSpans: true, allowFuzzyMatching: true); + using var matcher = PatternMatcher.CreatePatternMatcher("Goo", includeMatchedSpans: true, PatternMatcherKind.Fuzzy); matcher.Matches("Go"); // Ensure that the above call ended up caching the result. - Assert.True(((PatternMatcher.SimplePatternMatcher)matcher).GetTestAccessor().LastCacheResultIs(areSimilar: true, candidateText: "Go")); + Assert.True(((PatternMatcher.FuzzyPatternMatcher)matcher).GetTestAccessor().LastCacheResultIs(areSimilar: true, candidateText: "Go")); matcher.Matches("DefNotAMatch"); - Assert.True(((PatternMatcher.SimplePatternMatcher)matcher).GetTestAccessor().LastCacheResultIs(areSimilar: false, candidateText: "DefNotAMatch")); + Assert.True(((PatternMatcher.FuzzyPatternMatcher)matcher).GetTestAccessor().LastCacheResultIs(areSimilar: false, candidateText: "DefNotAMatch")); } private static ImmutableArray PartListToSubstrings(string identifier, in TemporaryArray parts) @@ -459,7 +458,7 @@ private static ImmutableArray BreakIntoWordParts(string identifier) { MarkupTestFile.GetSpans(candidate, out candidate, out var spans); - var match = PatternMatcher.CreatePatternMatcher(pattern, includeMatchedSpans: true, allowFuzzyMatching: false) + var match = PatternMatcher.CreatePatternMatcher(pattern, includeMatchedSpans: true) .GetFirstMatch(candidate); if (match == null) diff --git a/src/EditorFeatures/Test2/Copilot/RoslynProposalAdjusterTests.vb b/src/EditorFeatures/Test2/Copilot/RoslynProposalAdjusterTests.vb index c637fb2e8c9d..b11b5b717b76 100644 --- a/src/EditorFeatures/Test2/Copilot/RoslynProposalAdjusterTests.vb +++ b/src/EditorFeatures/Test2/Copilot/RoslynProposalAdjusterTests.vb @@ -52,7 +52,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.Copilot Dim service = originalDocument.GetRequiredLanguageService(Of ICopilotProposalAdjusterService) Dim tuple = Await service.TryAdjustProposalAsync( - allowableAdjustments:=fixers, originalDocument, CopilotUtilities.TryNormalizeCopilotTextChanges(changes), CancellationToken.None) + allowableAdjustments:=fixers, originalDocument, CopilotUtilities.TryNormalizeCopilotTextChanges(changes), lineFormattingOptions:=Nothing, CancellationToken.None) Dim adjustedChanges = tuple.TextChanges Dim format = tuple.Format @@ -559,6 +559,300 @@ class C }", {ProposalAdjusterKinds.FormatCode}.ToImmutableHashSet()) End Function + + Public Async Function TestCSharp_LineEndingsPreserved_LfOnly() As Task + ' Ensure that when the original document uses LF-only line endings, the adjusted + ' text changes also use LF-only line endings (not CRLF). + Await TestLineEndingsPreserved(vbLf) + End Function + + + Public Async Function TestCSharp_LineEndingsPreserved_CrLf() As Task + ' Ensure that when the original document uses CRLF line endings, the adjusted + ' text changes also use CRLF line endings. + Await TestLineEndingsPreserved(vbCrLf) + End Function + + Private Shared Async Function TestLineEndingsPreserved(lineEnding As String) As Task + ' Build a C# source file that uses the specified line ending and that will + ' trigger the AddMissingImports adjuster + Dim nl = lineEnding + Dim originalCode = + "class C" & nl & + "{" & nl & + " void M()" & nl & + " {" & nl & + " }" & nl & + "}" + + Dim proposalText = "Console.WriteLine(1);" + + Using workspace = EditorTestWorkspace.CreateCSharp( + originalCode, composition:=s_composition) + Dim documentId = workspace.Documents.First().Id + Dim originalDocument = workspace.CurrentSolution.GetDocument(documentId) + Dim originalText = Await originalDocument.GetTextAsync() + + ' Verify the document actually has the line endings we expect. + Assert.True(originalText.ToString().Contains(lineEnding)) + + Dim insertPos = originalCode.IndexOf(" {" & nl, originalCode.IndexOf("void M()")) + (" {" & nl).Length + Dim changes = CopilotUtilities.TryNormalizeCopilotTextChanges( + {New TextChange(New TextSpan(insertPos, 0), " " & proposalText & nl)}) + + Dim fixers = { + ProposalAdjusterKinds.AddMissingImports, + ProposalAdjusterKinds.FormatCode + }.ToImmutableHashSet() + + Dim service = originalDocument.GetRequiredLanguageService(Of ICopilotProposalAdjusterService) + Dim result = Await service.TryAdjustProposalAsync( + allowableAdjustments:=fixers, originalDocument, changes, lineFormattingOptions:=Nothing, CancellationToken.None) + + ' The adjuster should have made changes (at minimum, adding "using System;"). + Assert.False(result.TextChanges.IsDefaultOrEmpty) + + ' Verify that every line ending in every TextChange.NewText matches the + ' original document's line ending style. + For Each change In result.TextChanges + Dim newText = change.NewText + If newText Is Nothing Then Continue For + + If lineEnding = vbLf Then + ' LF-only: there should be no CR characters at all. + Assert.DoesNotContain(vbCr, newText) + Else + ' CRLF: every LF should be preceded by a CR. + For i = 0 To newText.Length - 1 + If newText(i) = CChar(vbLf) Then + Assert.True(i > 0 AndAlso newText(i - 1) = CChar(vbCr), + $"Found bare LF at position {i} in TextChange.NewText: ""{newText}""") + End If + Next + End If + Next + End Using + End Function + + + Public Async Function TestCSharp_LineEndingsPreserved_Mixed() As Task + ' Build a document that uses CRLF for most lines but LF for the line inside the + ' method body. The adjuster should preserve each line's original ending in the + ' corresponding TextChange, even when they differ within a single change span. + Dim crlf = vbCrLf + Dim lf = vbLf + Dim originalCode = + "class C" & crlf & + "{" & crlf & + " void M()" & crlf & + " {" & lf & + " }" & crlf & + "}" + + Dim proposalText = "Console.WriteLine(1);" + + Using workspace = EditorTestWorkspace.CreateCSharp( + originalCode, composition:=s_composition) + Dim documentId = workspace.Documents.First().Id + Dim originalDocument = workspace.CurrentSolution.GetDocument(documentId) + Dim originalText = Await originalDocument.GetTextAsync() + + ' The document should contain both CRLF and bare-LF. + Dim originalString = originalText.ToString() + Assert.Contains(crlf, originalString) + Assert.True(originalString.Contains(" {" & lf)) + + Dim insertPos = originalCode.IndexOf(" {" & lf) + (" {" & lf).Length + Dim changes = CopilotUtilities.TryNormalizeCopilotTextChanges( + {New TextChange(New TextSpan(insertPos, 0), " " & proposalText & lf)}) + + Dim fixers = { + ProposalAdjusterKinds.AddMissingImports, + ProposalAdjusterKinds.FormatCode + }.ToImmutableHashSet() + + Dim service = originalDocument.GetRequiredLanguageService(Of ICopilotProposalAdjusterService) + Dim result = Await service.TryAdjustProposalAsync( + allowableAdjustments:=fixers, originalDocument, changes, lineFormattingOptions:=Nothing, CancellationToken.None) + + Assert.False(result.TextChanges.IsDefaultOrEmpty) + + ' For each change, collect the line endings from the original text within + ' the change's span, then verify the NewText uses those same endings in order. + For Each change In result.TextChanges + Dim newText = change.NewText + If newText Is Nothing Then Continue For + + ' Collect expected line endings from the original span. + Dim expectedEndings = New List(Of String)() + Dim startLine = originalText.Lines.GetLineFromPosition(change.Span.Start).LineNumber + Dim endLine = originalText.Lines.GetLineFromPosition(change.Span.End).LineNumber + For i = startLine To endLine + Dim line = originalText.Lines(i) + If line.End >= change.Span.Start AndAlso line.End < change.Span.End Then + Dim breakLen = line.EndIncludingLineBreak - line.End + If breakLen = 2 Then + expectedEndings.Add(crlf) + ElseIf breakLen = 1 Then + expectedEndings.Add(If(originalString(line.End) = CChar(vbLf), lf, vbCr)) + End If + End If + Next + + ' Collect actual line endings from the new text. + Dim actualEndings = New List(Of String)() + Dim j = 0 + While j < newText.Length + If newText(j) = CChar(vbCr) AndAlso j + 1 < newText.Length AndAlso newText(j + 1) = CChar(vbLf) Then + actualEndings.Add(crlf) + j += 2 + ElseIf newText(j) = CChar(vbLf) Then + actualEndings.Add(lf) + j += 1 + ElseIf newText(j) = CChar(vbCr) Then + actualEndings.Add(vbCr) + j += 1 + Else + j += 1 + End If + End While + + ' The endings that overlap with original span positions should match 1:1. + For i = 0 To Math.Min(expectedEndings.Count, actualEndings.Count) - 1 + Assert.Equal(expectedEndings(i), actualEndings(i)) + Next + Next + End Using + End Function + + + Public Async Function TestCSharp_LineFormattingOptions_OverridesDocumentNewLine() As Task + ' A CRLF document, but we pass LineFormattingOptions with LF as the newline. + Dim crlf = vbCrLf + Dim lf = vbLf + + Dim originalCode = + "class C" & crlf & + "{" & crlf & + " void M()" & crlf & + " {" & crlf & + " }" & crlf & + "}" + + Dim proposalText = "Console.WriteLine(1);" + + Using workspace = EditorTestWorkspace.CreateCSharp( + originalCode, composition:=s_composition) + Dim documentId = workspace.Documents.First().Id + Dim originalDocument = workspace.CurrentSolution.GetDocument(documentId) + + Dim insertPos = originalCode.IndexOf(" {" & crlf, originalCode.IndexOf("void M()")) + (" {" & crlf).Length + Dim changes = CopilotUtilities.TryNormalizeCopilotTextChanges( + {New TextChange(New TextSpan(insertPos, 0), " " & proposalText & crlf)}) + + ' Pass LineFormattingOptions that say the file uses LF. + Dim lfOptions = New LineFormattingOptions() With {.NewLine = lf} + + Dim fixers = { + ProposalAdjusterKinds.AddMissingImports, + ProposalAdjusterKinds.FormatCode + }.ToImmutableHashSet() + + Dim service = originalDocument.GetRequiredLanguageService(Of ICopilotProposalAdjusterService) + Dim result = Await service.TryAdjustProposalAsync( + allowableAdjustments:=fixers, originalDocument, changes, lineFormattingOptions:=lfOptions, CancellationToken.None) + + Assert.False(result.TextChanges.IsDefaultOrEmpty) + + Dim usingChange = result.TextChanges.FirstOrDefault( + Function(c) c.NewText IsNot Nothing AndAlso c.NewText.Contains("using System")) + Assert.NotNull(usingChange.NewText) + End Using + End Function + + + Public Sub TestCSharp_FixLineEndingBoundaries_NewTextStartsWithLfAfterCr() + ' "AB\r\nCD\r\nEF" - insert "\nX" at position 3 (between \r and \n). + ' NewText[0]=\n and preceding char is \r, so would be rejected. + ' The leading \n is dropped. + Dim originalText = SourceText.From("AB" & vbCrLf & "CD" & vbCrLf & "EF") + Dim changes = ImmutableArray.Create( + New TextChange(New TextSpan(3, 0), vbLf & "X")) + + Dim fixed = AbstractCopilotProposalAdjusterService.TestAccessor.FixLineEndingBoundaries(originalText, changes) + Assert.Single(fixed) + Assert.Equal(4, fixed(0).Span.Start) + Assert.Equal(4, fixed(0).Span.End) + Assert.Equal("X", fixed(0).NewText) + + Dim result = originalText.WithChanges(fixed) + Assert.Equal("AB" & vbCrLf & "XCD" & vbCrLf & "EF", result.ToString()) + End Sub + + + Public Sub TestCSharp_FixLineEndingBoundaries_NewTextEndsWithCrBeforeLf() + ' "AB\r\nCD\r\nEF" - insert "X\r" at position 7 (between \r and \n). + ' NewText[^1]=\r and following char is \n, so would be rejected. + ' The trailing \r is dropped. + Dim originalText = SourceText.From("AB" & vbCrLf & "CD" & vbCrLf & "EF") + Dim changes = ImmutableArray.Create( + New TextChange(New TextSpan(7, 0), "X" & vbCr)) + + Dim fixed = AbstractCopilotProposalAdjusterService.TestAccessor.FixLineEndingBoundaries(originalText, changes) + Assert.Single(fixed) + Assert.Equal(6, fixed(0).Span.Start) + Assert.Equal(6, fixed(0).Span.End) + Assert.Equal("X", fixed(0).NewText) + + Dim result = originalText.WithChanges(fixed) + Assert.Equal("AB" & vbCrLf & "CDX" & vbCrLf & "EF", result.ToString()) + End Sub + + + Public Sub TestCSharp_FixLineEndingBoundaries_NoBoundaryIssue() + ' No boundary issue, returned unchanged. + Dim originalText = SourceText.From("AB" & vbCrLf & "CD" & vbCrLf & "EF") + Dim changes = ImmutableArray.Create( + New TextChange(New TextSpan(4, 2), "XY")) + + Dim fixed = AbstractCopilotProposalAdjusterService.TestAccessor.FixLineEndingBoundaries(originalText, changes) + Assert.Single(fixed) + Assert.Equal(4, fixed(0).Span.Start) + Assert.Equal(6, fixed(0).Span.End) + Assert.Equal("XY", fixed(0).NewText) + + Dim result = originalText.WithChanges(fixed) + Assert.Equal("AB" & vbCrLf & "XY" & vbCrLf & "EF", result.ToString()) + End Sub + + + Public Sub TestCSharp_FixLineEndingBoundaries_AdjacentChangesSplitCrLf() + ' Two adjacent changes split a \r\n pair across the boundary. + ' "ABC\r" + "\nDEF" - the trailing \r and leading \n are dropped, + ' and the spans are shrunk since the original chars match. + Dim originalText = SourceText.From("abc" & vbCrLf & "def") + Dim changes = ImmutableArray.Create( + New TextChange(New TextSpan(0, 4), "ABC" & vbCr), + New TextChange(New TextSpan(4, 4), vbLf & "DEF")) + + Dim fixed = AbstractCopilotProposalAdjusterService.TestAccessor.FixLineEndingBoundaries(originalText, changes) + Assert.Equal(2, fixed.Length) + + ' First change: trailing \r dropped, span shrunk from [0,4) to [0,3). + Assert.Equal(0, fixed(0).Span.Start) + Assert.Equal(3, fixed(0).Span.End) + Assert.Equal("ABC", fixed(0).NewText) + + ' Second change: leading \n dropped, span shrunk from [4,8) to [5,8). + Assert.Equal(5, fixed(1).Span.Start) + Assert.Equal(8, fixed(1).Span.End) + Assert.Equal("DEF", fixed(1).NewText) + + ' The original \r\n at positions 3-4 is preserved. + Dim result = originalText.WithChanges(fixed) + Assert.Equal("ABC" & vbCrLf & "DEF", result.ToString()) + End Sub + #End Region #Region "Visual Basic" diff --git a/src/EditorFeatures/Test2/FindReferences/FindReferencesTests.NamedTypeSymbols.vb b/src/EditorFeatures/Test2/FindReferences/FindReferencesTests.NamedTypeSymbols.vb index be0747edce69..09f6dba39aa2 100644 --- a/src/EditorFeatures/Test2/FindReferences/FindReferencesTests.NamedTypeSymbols.vb +++ b/src/EditorFeatures/Test2/FindReferences/FindReferencesTests.NamedTypeSymbols.vb @@ -2603,5 +2603,182 @@ namespace N Await TestAPIAndFeature(input, kind, host) End Function + + + Public Async Function TestUnionDeclaration_FindUnionType(kind As TestKind, host As TestHost) As Task + Dim input = + + + + + + + Await TestAPIAndFeature(input, kind, host) + End Function + + + Public Async Function TestUnionDeclaration_FindTypeInUnionParameterList(kind As TestKind, host As TestHost) As Task + Dim input = + + + + + + + Await TestAPIAndFeature(input, kind, host) + End Function + + + Public Async Function TestUnionDeclaration_FindSecondTypeInUnionParameterList(kind As TestKind, host As TestHost) As Task + Dim input = + + + + + + + Await TestAPIAndFeature(input, kind, host) + End Function + + + Public Async Function TestUnionDeclaration_FindGenericUnionType(kind As TestKind, host As TestHost) As Task + Dim input = + + + (T1, T2) { } + +class C +{ + [|Result|] M() => default; +} +]]> + + + + Await TestAPIAndFeature(input, kind, host) + End Function + + + Public Async Function TestUnionDeclaration_FindTypeParameterInUnion(kind As TestKind, host As TestHost) As Task + Dim input = + + + ([|T1|], T2) { } +]]> + + + + Await TestAPIAndFeature(input, kind, host) + End Function + + + Public Async Function TestUnionDeclaration_FindUnionTypeFromUsage(kind As TestKind, host As TestHost) As Task + Dim input = + + + + + + + Await TestAPIAndFeature(input, kind, host) + End Function + + + Public Async Function TestUnionDeclaration_FindTypeInUnionParameterListFromUsage(kind As TestKind, host As TestHost) As Task + Dim input = + + + + + + + Await TestAPIAndFeature(input, kind, host) + End Function + + + Public Async Function TestUnionDeclaration_NonDependentProject(kind As TestKind, host As TestHost) As Task + ' Union defined in one project, referenced by name in a non-dependent project. + Dim input = + + + + + + + + + + + Await TestAPIAndFeature(input, kind, host) + End Function + + + Public Async Function TestUnionDeclaration_InDependentProject(kind As TestKind, host As TestHost) As Task + ' Union defined in one project, used in a dependent project via ProjectReference. + Dim input = + + + + + + + CSharpAssembly1 + + + + + Await TestAPIAndFeature(input, kind, host) + End Function + End Class End Namespace diff --git a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb index 77f206af26a8..e26e7d90f45b 100644 --- a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb +++ b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb @@ -13783,5 +13783,68 @@ namespace NS Assert.Equal(SymbolMatchPriority.PreferFieldOrProperty, item.Rules.MatchPriority) End Using End Function + + + + Public Async Function PropertyWithIdenticalNamedExtensionMethod() As Task + Using state = TestStateFactory.CreateCSharpTestState( + 42; +} + +public static class Extensions +{ + public static int Age(this TestClass c) => 100; +} + +internal class Program +{ + static void Main(string[] args) + { + var t = new TestClass(); + var x = t.$$ + } +} + ]]>) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContain(Function(i) i.DisplayText = "Age" AndAlso i.Tags.Contains(WellKnownTags.Property)) + Await state.AssertCompletionItemsContain(Function(i) i.DisplayText = "Age" AndAlso i.Tags.Contains(WellKnownTags.ExtensionMethod)) + End Using + End Function + + + + Public Async Function MethodWithIdenticalNamedExtensionProperty() As Task + Using state = TestStateFactory.CreateCSharpTestState( + 42; +} + +public static class Extensions +{ + extension(TestClass testclass) + { + public int Age => 0; + } +} + +internal class Program +{ + static void Main(string[] args) + { + var t = new TestClass(); + var x = t.$$ + } +} + ]]>) + state.SendInvokeCompletionList() + Await state.AssertCompletionItemsContain(Function(i) i.DisplayText = "Age" AndAlso i.Tags.Contains(WellKnownTags.Method)) + Await state.AssertCompletionItemsContain(Function(i) i.DisplayText = "Age" AndAlso i.Tags.Contains(WellKnownTags.Property)) + End Using + End Function End Class End Namespace diff --git a/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests.vb b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests.vb index c4319d68e069..da871efe2f62 100644 --- a/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests.vb +++ b/src/EditorFeatures/Test2/IntelliSense/IntellisenseQuickInfoBuilderTests.vb @@ -1278,6 +1278,132 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense ToolTipAssert.EqualContent(expected, container) End Function + ' Tracked by https//github.com/dotnet/roslyn/issues/82607 + ' Consider displaying unions as `union MyUnion` instead of `struct MyUnion` and with a separate classification type + ' Consider listing case types in QuickInfo tooltip for unions + + Public Async Function QuickInfoForUnions_01() As Task + Dim workspace = + + + + union TestUnion(int, string) { } + + class C + { + void M() + { + Test$$Union x = default; + } + } + + + + + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) + Assert.NotNull(intellisenseQuickInfo) + + Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) + + Dim expected = New ContainerElement( + ContainerElementStyle.Stacked Or ContainerElementStyle.VerticalPadding, + New ContainerElement( + ContainerElementStyle.Wrapped, + New ImageElement(New ImageId(KnownImageIds.ImageCatalogGuid, KnownImageIds.ValueTypeInternal)), + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Keyword, "struct"), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.StructName, "TestUnion", navigationAction:=Sub() Return, "TestUnion")))) + + ToolTipAssert.EqualContent(expected, container) + End Function + + + Public Async Function QuickInfoForUnions_02() As Task + Dim workspace = + + + + [System.Runtime.CompilerServices.Union] + class TestUnion { } + + class C + { + void M() + { + Test$$Union x = default; + } + } + + namespace System.Runtime.CompilerServices + { + public class UnionAttribute : System.Attribute { } + } + + + + + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) + Assert.NotNull(intellisenseQuickInfo) + + Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) + + Dim expected = New ContainerElement( + ContainerElementStyle.Stacked Or ContainerElementStyle.VerticalPadding, + New ContainerElement( + ContainerElementStyle.Wrapped, + New ImageElement(New ImageId(KnownImageIds.ImageCatalogGuid, KnownImageIds.ClassInternal)), + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Keyword, "class"), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.ClassName, "TestUnion", navigationAction:=Sub() Return, "TestUnion")))) + + ToolTipAssert.EqualContent(expected, container) + End Function + + + Public Async Function QuickInfoForUnions_03() As Task + Dim workspace = + + + + [System.Runtime.CompilerServices.Union] + struct TestUnion { } + + class C + { + void M() + { + Test$$Union x = default; + } + } + + namespace System.Runtime.CompilerServices + { + public class UnionAttribute : System.Attribute { } + } + + + + + Dim intellisenseQuickInfo = Await GetQuickInfoItemAsync(workspace, LanguageNames.CSharp) + Assert.NotNull(intellisenseQuickInfo) + + Dim container = Assert.IsType(Of ContainerElement)(intellisenseQuickInfo.Item) + + Dim expected = New ContainerElement( + ContainerElementStyle.Stacked Or ContainerElementStyle.VerticalPadding, + New ContainerElement( + ContainerElementStyle.Wrapped, + New ImageElement(New ImageId(KnownImageIds.ImageCatalogGuid, KnownImageIds.ValueTypeInternal)), + New ClassifiedTextElement( + New ClassifiedTextRun(ClassificationTypeNames.Keyword, "struct"), + New ClassifiedTextRun(ClassificationTypeNames.WhiteSpace, " "), + New ClassifiedTextRun(ClassificationTypeNames.StructName, "TestUnion", navigationAction:=Sub() Return, "TestUnion")))) + + ToolTipAssert.EqualContent(expected, container) + End Function + Public Async Function QuickInfoForAlias1() As Task Dim workspace = diff --git a/src/EditorFeatures/Test2/Rename/RenameEngineTests.vb b/src/EditorFeatures/Test2/Rename/RenameEngineTests.vb index 9753cb7d240e..68cba3105385 100644 --- a/src/EditorFeatures/Test2/Rename/RenameEngineTests.vb +++ b/src/EditorFeatures/Test2/Rename/RenameEngineTests.vb @@ -2033,6 +2033,48 @@ End Module End Using End Sub + + Public Sub RenameTypeFromConversionOperatorReturnType(host As RenameTestHost) + Using result = RenameEngineResult.Create(_outputHelper, + + + +class C; + +class [|Repro1|] +{ + public static explicit operator [|$$Repro1|](C _) + { + return null; + } +} + + + , host:=host, renameTo:="Renamed") + + End Using + End Sub + + + Public Sub RenameTypeFromGenericConversionOperatorReturnType(host As RenameTestHost) + Using result = RenameEngineResult.Create(_outputHelper, + + + +class [|Repro2|]<T> +{ + public static explicit operator [|$$Repro2|]<T>(T _) + { + return null; + } +} + + + , host:=host, renameTo:="Renamed") + + End Using + End Sub + Public Sub RenameMethodThatImplementsInterfaceMethod(host As RenameTestHost) diff --git a/src/EditorFeatures/TestUtilities/GoToAdjacentMember/AbstractGoToAdjacentMemberTests.cs b/src/EditorFeatures/TestUtilities/GoToAdjacentMember/AbstractGoToAdjacentMemberTests.cs index 427bcee85dcc..0c415b950723 100644 --- a/src/EditorFeatures/TestUtilities/GoToAdjacentMember/AbstractGoToAdjacentMemberTests.cs +++ b/src/EditorFeatures/TestUtilities/GoToAdjacentMember/AbstractGoToAdjacentMemberTests.cs @@ -8,9 +8,15 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Editor.UnitTests.Extensions; +using Microsoft.CodeAnalysis.Editor.UnitTests.Utilities; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.Commanding; +using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; using Xunit; namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToAdjacentMember; @@ -29,25 +35,52 @@ protected async Task AssertNavigatedAsync(string code, bool next, SourceCodeKind foreach (var kind in kinds) { - using var workspace = TestWorkspace.Create( + using var workspace = EditorTestWorkspace.Create( LanguageName, compilationOptions: null, parseOptions: DefaultParseOptions.WithKind(kind), content: code); + var hostDocument = workspace.DocumentWithCursor; var document = workspace.CurrentSolution.GetDocument(hostDocument.Id); var parsedDocument = await ParsedDocument.CreateAsync(document, CancellationToken.None); Assert.Empty(parsedDocument.SyntaxTree.GetDiagnostics()); - var service = document.GetRequiredLanguageService(); - var targetPosition = GoToAdjacentMemberCommandHandler.GetTargetPosition( - service, - parsedDocument.Root, - hostDocument.CursorPosition.Value, - next); + var textView = hostDocument.GetTextView(); + var subjectBuffer = hostDocument.GetTextBuffer(); + + if (hostDocument.AnnotatedSpans.TryGetValue("selection", out var annotatedSelection) && annotatedSelection.Any()) + { + var span = annotatedSelection.SingleOrDefault(); + var cursorPosition = hostDocument.CursorPosition ?? span.Start; + + textView.Selection.Mode = TextSelectionMode.Stream; + + var snapshotSpan = new SnapshotSpan(subjectBuffer.CurrentSnapshot, span.Start, span.Length); + var isReversed = cursorPosition == span.Start; + + textView.Selection.Select(snapshotSpan, isReversed); + } + else + { + textView.Caret.MoveTo(new SnapshotPoint(subjectBuffer.CurrentSnapshot, hostDocument.CursorPosition.Value)); + } + + var handler = workspace.ExportProvider.GetCommandHandler(PredefinedCommandHandlerNames.GoToAdjacentMember, ContentTypeNames.RoslynContentType); + + EditorCommandArgs args = next + ? new GoToNextMemberCommandArgs(textView, subjectBuffer) + : new GoToPreviousMemberCommandArgs(textView, subjectBuffer); + + var executed = next + ? handler.ExecuteCommand((GoToNextMemberCommandArgs)args, TestCommandExecutionContext.Create()) + : handler.ExecuteCommand((GoToPreviousMemberCommandArgs)args, TestCommandExecutionContext.Create()); + + Assert.True(executed, "Command handler should have executed."); + + var finalPosition = textView.Caret.Position.BufferPosition.Position; - Assert.NotNull(targetPosition); - Assert.Equal(hostDocument.SelectedSpans.Single().Start, targetPosition.Value); + Assert.Equal(hostDocument.SelectedSpans.Single().Start, finalPosition); } } diff --git a/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj b/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj index 6d412bd59c5d..c6add8f2eee5 100644 --- a/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj +++ b/src/EditorFeatures/TestUtilities/Microsoft.CodeAnalysis.EditorFeatures.Test.Utilities.csproj @@ -44,7 +44,6 @@ - diff --git a/src/EditorFeatures/Text/Implementation/SourceTextReader.cs b/src/EditorFeatures/Text/Implementation/SourceTextReader.cs new file mode 100644 index 000000000000..9f6385effb09 --- /dev/null +++ b/src/EditorFeatures/Text/Implementation/SourceTextReader.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; + +namespace Microsoft.CodeAnalysis.Text.Implementation; + +internal sealed class SourceTextReader(SourceText sourceText) : TextReader +{ + private readonly SourceText _sourceText = sourceText; + private int _position = 0; + + public override int Peek() + { + return _position == _sourceText.Length + ? -1 + : _sourceText[_position]; + } + + public override int Read() + { + return _position == _sourceText.Length + ? -1 + : _sourceText[_position++]; + } + + public override int Read(char[] buffer, int index, int count) + { + var charsToCopy = Math.Min(count, _sourceText.Length - _position); + _sourceText.CopyTo(_position, buffer, index, charsToCopy); + _position += charsToCopy; + return charsToCopy; + } +} diff --git a/src/EditorFeatures/Text/Implementation/TextBufferFactoryService/TextBufferCloneServiceFactory.cs b/src/EditorFeatures/Text/Implementation/TextBufferFactoryService/TextBufferCloneServiceFactory.cs index 3357ab043a87..b770c45f6f70 100644 --- a/src/EditorFeatures/Text/Implementation/TextBufferFactoryService/TextBufferCloneServiceFactory.cs +++ b/src/EditorFeatures/Text/Implementation/TextBufferFactoryService/TextBufferCloneServiceFactory.cs @@ -41,15 +41,16 @@ public ITextBuffer CloneWithRoslynContentType(SourceText sourceText) public ITextBuffer Clone(SourceText sourceText, IContentType contentType) { - // see whether we can do it cheaply + // see whether we can do directly via a text image var textImage = sourceText.TryFindCorrespondingEditorTextImage(); if (textImage != null) { return Clone(textImage, contentType); } - // we can't, so do it more expensive way - return _textBufferFactoryService.CreateTextBuffer(sourceText.ToString(), contentType); + // we can't, so do it through a text reader. + var reader = new SourceTextReader(sourceText); + return _textBufferFactoryService.CreateTextBuffer(reader, contentType); } private ITextBuffer Clone(ITextImage textImage, IContentType contentType) diff --git a/src/EditorFeatures/VisualBasicTest/GoToAdjacentMember/VisualBasicGoToAdjacentMemberTests.vb b/src/EditorFeatures/VisualBasicTest/GoToAdjacentMember/VisualBasicGoToAdjacentMemberTests.vb index bf03e81ce1e5..1ba331f7cf16 100644 --- a/src/EditorFeatures/VisualBasicTest/GoToAdjacentMember/VisualBasicGoToAdjacentMemberTests.vb +++ b/src/EditorFeatures/VisualBasicTest/GoToAdjacentMember/VisualBasicGoToAdjacentMemberTests.vb @@ -25,7 +25,7 @@ End Class" Assert.Null(Await GetTargetPositionAsync(code, next:=True)) End Function - + Public Async Function BeforeClassWithMember() As Task Dim code = "$$ Class C @@ -36,7 +36,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function AfterClassWithMember() As Task Dim code = " Class C @@ -49,7 +49,7 @@ $$" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function BetweenClasses() As Task Dim code = " Class C1 @@ -67,7 +67,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function BetweenClassesPrevious() As Task Dim code = " Class C1 @@ -85,7 +85,7 @@ End Class" Await AssertNavigatedAsync(code, next:=False) End Function - + Public Async Function FromFirstMemberToSecond() As Task Dim code = " Class C @@ -98,7 +98,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function FromSecondToFirst() As Task Dim code = " Class C @@ -111,7 +111,7 @@ End Class" Await AssertNavigatedAsync(code, next:=False) End Function - + Public Async Function NextWraps() As Task Dim code = " Class C @@ -124,7 +124,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function PreviousWraps() As Task Dim code = " Class C @@ -137,7 +137,7 @@ End Class" Await AssertNavigatedAsync(code, next:=False) End Function - + Public Async Function DescendsIntoNestedType() As Task Dim code = " Class C @@ -153,7 +153,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function StopsAtConstructor() As Task Dim code = " Class C @@ -166,7 +166,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function StopsAtOperator() As Task Dim code = " Class C @@ -183,7 +183,7 @@ End Class" Throw New System.NotImplementedException() End Operator - + Public Async Function StopsAtField() As Task Dim code = " Class C @@ -195,7 +195,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function StopsAtFieldlikeEvent() As Task Dim code = " Class C @@ -207,7 +207,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function StopsAtAutoProperty() As Task Dim code = " Class C @@ -218,7 +218,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function StopsAtPropertyWithAccessors() As Task Dim code = " Class C @@ -237,7 +237,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function SkipsPropertyAccessors() As Task Dim code = " Class C @@ -259,7 +259,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function FromInsidePropertyAccessor() As Task Dim code = " Class C @@ -281,7 +281,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function StopsAtEventWithAddRemove() As Task Dim code = " Class C @@ -304,7 +304,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function SkipsEventAddRemove() As Task Dim code = " Class C @@ -330,7 +330,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function FromInsideMethod() As Task Dim code = " Class C @@ -345,7 +345,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function NextFromBetweenMethods() As Task Dim code = " Class C @@ -361,7 +361,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function PreviousFromBetweenMethods() As Task Dim code = " Class C @@ -377,7 +377,7 @@ End Class" Await AssertNavigatedAsync(code, next:=False) End Function - + Public Async Function NextFromBetweenMethodsInTrailingTrivia() As Task Dim code = " Class C @@ -391,7 +391,7 @@ End Class" Await AssertNavigatedAsync(code, next:=True) End Function - + Public Async Function PreviousFromInsideCurrent() As Task Dim code = " @@ -407,7 +407,7 @@ End Class" Await AssertNavigatedAsync(code, next:=False) End Function - + Public Async Function PreviousFromBetweenMethodsInTrailingTrivia() As Task Dim code = " Class C @@ -421,7 +421,7 @@ End Class" Await AssertNavigatedAsync(code, next:=False) End Function - + Public Async Function NextInScript() As Task Dim code = " $$Sub M1() @@ -433,7 +433,7 @@ End Sub" Await AssertNavigatedAsync(code, next:=True, sourceCodeKind:=SourceCodeKind.Script) End Function - + Public Async Function PrevInScript() As Task Dim code = " [||]Sub M1() diff --git a/src/EditorFeatures/VisualBasicTest/NavigateTo/NavigateToTests.vb b/src/EditorFeatures/VisualBasicTest/NavigateTo/NavigateToTests.vb index 070f4441e7c0..2c3ddbf77d46 100644 --- a/src/EditorFeatures/VisualBasicTest/NavigateTo/NavigateToTests.vb +++ b/src/EditorFeatures/VisualBasicTest/NavigateTo/NavigateToTests.vb @@ -1,4 +1,4 @@ -' Licensed to the .NET Foundation under one or more agreements. +' Licensed to the .NET Foundation under one or more agreements. ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. @@ -49,8 +49,9 @@ End Class", Async Function(w) Dim item As NavigateToItem = (Await _aggregator.GetItemsAsync("class")).Single() VerifyNavigateToResultItem(item, "Class", "[|Class|]", PatternMatchKind.Exact, NavigateToItemKind.Class, Glyph.ClassInternal) - item = (Await _aggregator.GetItemsAsync("[class]")).Single() - VerifyNavigateToResultItem(item, "Class", "[|Class|]", PatternMatchKind.Exact, NavigateToItemKind.Class, Glyph.ClassInternal) + ' [class] is now treated as a regex character class (matching a single char from {c,l,a,s}). + ' It has no extractable 2+ char literals, so the regex search is skipped entirely. + Assert.Empty(Await _aggregator.GetItemsAsync("[class]")) End Function) End Function @@ -219,8 +220,8 @@ End Class", Async Function(w) Dim item As NavigateToItem = (Await _aggregator.GetItemsAsync("string")).Single() VerifyNavigateToResultItem(item, "string", "[|string|]", PatternMatchKind.Exact, NavigateToItemKind.Field, Glyph.FieldPrivate, additionalInfo:=String.Format(FeaturesResources.in_0_project_1, "Goo", "Test")) - item = (Await _aggregator.GetItemsAsync("[string]")).Single() - VerifyNavigateToResultItem(item, "string", "[|string|]", PatternMatchKind.Exact, NavigateToItemKind.Field, Glyph.FieldPrivate, additionalInfo:=String.Format(FeaturesResources.in_0_project_1, "Goo", "Test")) + ' [string] is now treated as a regex character class. + Assert.Empty(Await _aggregator.GetItemsAsync("[string]")) End Function) End Function @@ -315,8 +316,8 @@ End Class", Async Function(w) Dim item As NavigateToItem = (Await _aggregator.GetItemsAsync("sub")).Single() VerifyNavigateToResultItem(item, "Sub", "[|Sub|]()", PatternMatchKind.Exact, NavigateToItemKind.Method, Glyph.MethodPrivate, String.Format(FeaturesResources.in_0_project_1, "Goo", "Test")) - item = (Await _aggregator.GetItemsAsync("[sub]")).Single() - VerifyNavigateToResultItem(item, "Sub", "[|Sub|]()", PatternMatchKind.Exact, NavigateToItemKind.Method, Glyph.MethodPrivate, String.Format(FeaturesResources.in_0_project_1, "Goo", "Test")) + ' [sub] is now treated as a regex character class. + Assert.Empty(Await _aggregator.GetItemsAsync("[sub]")) End Function) End Function diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs index 6844b466db7c..79c0cb22dd2b 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EECompilationContextMethod.cs @@ -60,6 +60,10 @@ public override ImmutableArray Parameters get { return _parameters; } } + public override bool IsAsync => false; + + internal sealed override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); + public override TypeWithAnnotations ReturnTypeWithAnnotations => _underlyingMethod.ReturnTypeWithAnnotations; public override ImmutableArray ExplicitInterfaceImplementations => ImmutableArray.Empty; diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EEMethodSymbol.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EEMethodSymbol.cs index 361028eb990e..a084260f037d 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EEMethodSymbol.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EEMethodSymbol.cs @@ -91,6 +91,7 @@ internal EEMethodSymbol( Debug.Assert(sourceMethod.IsDefinition); Debug.Assert(TypeSymbol.Equals((TypeSymbol)sourceMethod.ContainingSymbol, container.SubstitutedSourceType.OriginalDefinition, TypeCompareKind.ConsiderEverything2)); Debug.Assert(sourceLocals.All(l => l.ContainingSymbol == sourceMethod)); + Debug.Assert(!sourceMethod.IsAsync); _container = container; _name = name; @@ -312,7 +313,7 @@ public override bool IsVararg public override RefKind RefKind { - get { return this.SubstitutedSourceMethod.RefKind; } + get { return RefKind.None; } } public override bool ReturnsVoid @@ -325,6 +326,8 @@ public override bool IsAsync get { return false; } } + internal override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); + public override TypeWithAnnotations ReturnTypeWithAnnotations { get @@ -468,6 +471,8 @@ internal override ObsoleteAttributeData ObsoleteAttributeData internal override bool UseUpdatedEscapeRules => false; + internal override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal ResultProperties ResultProperties { get { return _lazyResultProperties; } diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EENamedTypeSymbol.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EENamedTypeSymbol.cs index 85d33ce5ce39..c3e8845ac51b 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EENamedTypeSymbol.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/EENamedTypeSymbol.cs @@ -134,7 +134,7 @@ public override NamedTypeSymbol ConstructedFrom get { return this; } } - public override bool MightContainExtensionMethods + public override bool MightContainExtensions { get { return false; } } @@ -350,6 +350,8 @@ internal override bool IsInterface internal override bool HasCompilerLoweringPreserveAttribute => false; + internal override bool IsUnionTypeCore => false; + internal sealed override NamedTypeSymbol AsNativeInteger() => throw ExceptionUtilities.Unreachable(); internal sealed override NamedTypeSymbol NativeIntegerUnderlyingType => null; diff --git a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/PlaceholderMethodSymbol.cs b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/PlaceholderMethodSymbol.cs index 1dc43850fcbf..5c96bc7a35fc 100644 --- a/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/PlaceholderMethodSymbol.cs +++ b/src/ExpressionEvaluator/CSharp/Source/ExpressionCompiler/Symbols/PlaceholderMethodSymbol.cs @@ -222,6 +222,8 @@ internal override ObsoleteAttributeData ObsoleteAttributeData internal override bool UseUpdatedEscapeRules => false; + internal override CallerUnsafeMode CallerUnsafeMode => CallerUnsafeMode.None; + internal override bool RequiresSecurityObject { get { return false; } @@ -279,6 +281,8 @@ internal override int CalculateLocalSyntaxOffset(int localPosition, SyntaxTree l protected override bool HasSetsRequiredMembersImpl => throw ExceptionUtilities.Unreachable(); + internal override ThreeState RuntimeAsyncMethodGenerationAttributeSetting => throw ExceptionUtilities.Unreachable(); + internal sealed override bool HasAsyncMethodBuilderAttribute(out TypeSymbol builderArgument) { builderArgument = null; diff --git a/src/ExpressionEvaluator/CSharp/Test/ExpressionCompiler/ExpressionCompilerTests.cs b/src/ExpressionEvaluator/CSharp/Test/ExpressionCompiler/ExpressionCompilerTests.cs index 132fdc0b7fa2..93b5abaa3953 100644 --- a/src/ExpressionEvaluator/CSharp/Test/ExpressionCompiler/ExpressionCompilerTests.cs +++ b/src/ExpressionEvaluator/CSharp/Test/ExpressionCompiler/ExpressionCompilerTests.cs @@ -793,6 +793,45 @@ .maxstack 2 }"); } + /// + /// Evaluating expressions inside a ref-returning method should + /// produce a query method that returns by value, not by ref. + /// + [Fact] + public void EvaluateExpressionInRefReturningMethod() + { + var source = +@"struct TestStruct +{ + public int Value; +} +class C +{ + static TestStruct _field; + ref readonly TestStruct GetValue(ulong id) + { + return ref _field; + } +}"; + var testData = Evaluate( + source, + OutputKind.DynamicallyLinkedLibrary, + methodName: "C.GetValue", + expr: "id"); + var methodData = testData.GetMethodData("<>x.<>m0"); + var method = (MethodSymbol)methodData.Method; + Assert.Equal(RefKind.None, method.RefKind); + + testData = Evaluate( + source, + OutputKind.DynamicallyLinkedLibrary, + methodName: "C.GetValue", + expr: "id == 1"); + methodData = testData.GetMethodData("<>x.<>m0"); + method = (MethodSymbol)methodData.Method; + Assert.Equal(RefKind.None, method.RefKind); + } + [Fact] public void EvaluateStaticMethodParameters() { diff --git a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx index 328ec89b4ea6..1d2a20543619 100644 --- a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx +++ b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx @@ -607,6 +607,10 @@ path 'path' is a placeholder for a project file or directory path in a directive like '#:project path'. + + path + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Adds an SDK reference. @@ -619,6 +623,9 @@ Adds a project reference. + + Adds a file reference. + Add 'using {0};' {Locked="using"} "using" is a C# keyword and should not be localized. @@ -627,4 +634,7 @@ Add 'using {0};' and simplify all occurrences {Locked="using"} "using" is a C# keyword and should not be localized. - + + unsafe block + + \ No newline at end of file diff --git a/src/Features/CSharp/Portable/CallHierarchy/CSharpCallHierarchyService.cs b/src/Features/CSharp/Portable/CallHierarchy/CSharpCallHierarchyService.cs new file mode 100644 index 000000000000..9d8633d04cd8 --- /dev/null +++ b/src/Features/CSharp/Portable/CallHierarchy/CSharpCallHierarchyService.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using Microsoft.CodeAnalysis.CallHierarchy; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.CSharp.CallHierarchy; + +[ExportLanguageService(typeof(ICallHierarchyService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpCallHierarchyService() : AbstractCallHierarchyService; diff --git a/src/Features/CSharp/Portable/CodeFixes/GenerateType/GenerateTypeCodeFixProvider.cs b/src/Features/CSharp/Portable/CodeFixes/GenerateType/GenerateTypeCodeFixProvider.cs index 217fdfbb42de..5fdd2611d5d9 100644 --- a/src/Features/CSharp/Portable/CodeFixes/GenerateType/GenerateTypeCodeFixProvider.cs +++ b/src/Features/CSharp/Portable/CodeFixes/GenerateType/GenerateTypeCodeFixProvider.cs @@ -32,20 +32,27 @@ internal sealed class GenerateTypeCodeFixProvider() : AbstractGenerateMemberCode private const string CS0308 = nameof(CS0308); // error CS0308: The non-generic type 'A' cannot be used with type arguments private const string CS0426 = nameof(CS0426); // error CS0426: The type name 'S' does not exist in the type 'Program' private const string CS0616 = nameof(CS0616); // error CS0616: 'x' is not an attribute class + private const string CS1574 = nameof(CS1574); // warning CS1574: XML comment has cref attribute that could not be resolved public override ImmutableArray FixableDiagnosticIds - => [CS0103, CS0117, CS0234, CS0246, CS0305, CS0308, CS0426, CS0616, IDEDiagnosticIds.UnboundIdentifierId]; + => [CS0103, CS0117, CS0234, CS0246, CS0305, CS0308, CS0426, CS0616, CS1574, IDEDiagnosticIds.UnboundIdentifierId]; protected override bool IsCandidate(SyntaxNode node, SyntaxToken token, Diagnostic diagnostic) => node switch { QualifiedNameSyntax or MemberAccessExpressionSyntax => true, SimpleNameSyntax simple => !simple.IsParentKind(SyntaxKind.QualifiedName), + TypeCrefSyntax => true, _ => false, }; protected override SyntaxNode? GetTargetNode(SyntaxNode node) - => ((ExpressionSyntax)node).GetRightmostName(); + => node switch + { + TypeCrefSyntax typeCref => typeCref.Type, + ExpressionSyntax expression => expression.GetRightmostName(), + _ => null, + }; protected override Task> GetCodeActionsAsync( Document document, SyntaxNode node, CancellationToken cancellationToken) diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/FileBasedPrograms/IncludeAppDirectiveCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/FileBasedPrograms/IncludeAppDirectiveCompletionProvider.cs new file mode 100644 index 000000000000..69bfda1dce9f --- /dev/null +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/FileBasedPrograms/IncludeAppDirectiveCompletionProvider.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Collections.Immutable; +using System.Composition; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.CSharp.Completion.Providers; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.Utilities; +using System.Linq; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Collections; + +namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; + +[ExportCompletionProvider(nameof(IncludeAppDirectiveCompletionProvider), LanguageNames.CSharp), Shared] +[ExtensionOrder(After = nameof(ProjectAppDirectiveCompletionProvider))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class IncludeAppDirectiveCompletionProvider() : AbstractAppDirectiveCompletionProvider +{ + protected override string DirectiveKind => "include"; + + protected sealed override void AddDirectiveKindCompletion(CompletionContext context) + { + context.AddItem(CommonCompletionItem.Create(DirectiveKind, displayTextSuffix: "", CompletionItemRules.Default, glyph: Glyph.Keyword, + description: [ + new(SymbolDisplayPartKind.Keyword, symbol: null, "#:include"), + new(SymbolDisplayPartKind.Space, symbol: null, " "), + new(SymbolDisplayPartKind.StringLiteral, symbol: null, CSharpFeaturesResources.Include_directive_file_path), + new(SymbolDisplayPartKind.LineBreak, symbol: null, ""), + new(SymbolDisplayPartKind.Text, symbol: null, CSharpFeaturesResources.Adds_a_file_reference), + ])); + } + + protected override async Task AddDirectiveContentCompletionsAsync(CompletionContext context, ReadOnlyMemory contentPrefix) + { + // Suppose we have a directive '#:include path/to/fi$$' + // In this case, 'contentPrefix' is 'path/to/fi'. + + var documentDirectory = PathUtilities.GetDirectoryName(context.Document.FilePath); + var baseDirectory = PathUtilities.IsAbsolute(documentDirectory) ? documentDirectory : null; + var fileSystemHelper = new FileSystemCompletionHelper( + Glyph.OpenFolder, + Glyph.CSharpFile, + searchPaths: [], + baseDirectory, + // Note: in the future, we may wish to use '' property + // as a hint for which file extensions to show in this completion. + // For now, we just allow any extension, and if user chooses a file with invalid extension, they'll just get a build error. + allowableExtensions: [], + CompletionItemRules.Default); + + var contentDirectory = PathUtilities.GetDirectoryName(contentPrefix.ToString()); + var items = await fileSystemHelper.GetItemsAsync(contentDirectory, context.CancellationToken).ConfigureAwait(false); + context.AddItems(items); + if (baseDirectory != null || PathUtilities.IsAbsolute(contentDirectory)) + { + addItem("*.cs"); + addItem("**/*.cs"); + } + + void addItem(string text) + { + context.AddItem(CommonCompletionItem.Create( + text, + displayTextSuffix: "", + glyph: Glyph.CSharpFile, + description: text.ToSymbolDisplayParts(), + rules: CompletionItemRules.Default)); + } + } +} diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs index 2d4c5bdae5e5..866000897c5f 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/KeywordCompletionProvider.cs @@ -153,6 +153,7 @@ internal sealed class KeywordCompletionProvider() : AbstractKeywordCompletionPro new ULongKeywordRecommender(), new UncheckedKeywordRecommender(), new UndefKeywordRecommender(), + new UnionKeywordRecommender(), new UnmanagedKeywordRecommender(), new UnsafeKeywordRecommender(), new UShortKeywordRecommender(), diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/LastBuiltInCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/LastBuiltInCompletionProvider.cs index 9b162e407735..ff25235f4f95 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/LastBuiltInCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/LastBuiltInCompletionProvider.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; /// provider does not provide any completions. /// [ExportCompletionProvider(nameof(LastBuiltInCompletionProvider), LanguageNames.CSharp)] -[ExtensionOrder(After = nameof(ProjectAppDirectiveCompletionProvider))] +[ExtensionOrder(After = nameof(IncludeAppDirectiveCompletionProvider))] [Shared] internal sealed class LastBuiltInCompletionProvider : CompletionProvider { diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs index 7e5201ae527f..095eb689f932 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/SnippetCompletionProvider.cs @@ -52,6 +52,7 @@ internal sealed class SnippetCompletionProvider() : LSPCompletionProvider CSharpSnippetIdentifiers.StaticIntMain, CSharpSnippetIdentifiers.Struct, CSharpSnippetIdentifiers.StaticVoidMain, + CSharpSnippetIdentifiers.Unsafe, CSharpSnippetIdentifiers.Using, CSharpSnippetIdentifiers.While ]; diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThisKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThisKeywordRecommender.cs index 09485863a583..4f0d0fdc6d3a 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThisKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/ThisKeywordRecommender.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Linq; using System.Threading; -using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -17,8 +15,7 @@ internal sealed class ThisKeywordRecommender() : AbstractSyntacticSingleKeywordR protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) => IsInstanceExpressionOrStatement(context) || IsThisParameterModifierContext(context) || - IsConstructorInitializerContext(context) || - IsNameofInsideAttributeContext(context); + IsConstructorInitializerContext(context); private static bool IsInstanceExpressionOrStatement(CSharpSyntaxContext context) => context.IsInstanceContext && (context.IsNonAttributeExpressionContext || context.IsStatementContext); @@ -58,25 +55,6 @@ or SyntaxKind.InKeyword return false; } - private static bool IsNameofInsideAttributeContext(CSharpSyntaxContext context) - { - // Fascinatingly, the language supports [Attr(nameof(this.X))] - - var token = context.TargetToken; - if (token.Kind() != SyntaxKind.OpenParenToken) - return false; - - if (!context.IsNameOfContext) - return false; - - var attribute = token.GetAncestor(); - if (attribute is null) - return false; - - var typeDeclaration = attribute.GetAncestor(); - return typeDeclaration != null; - } - protected override bool ShouldPreselect(CSharpSyntaxContext context, CancellationToken cancellationToken) { var outerType = context.SemanticModel.GetEnclosingNamedType(context.Position, cancellationToken); diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnionKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnionKeywordRecommender.cs new file mode 100644 index 000000000000..a42d0df750d8 --- /dev/null +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/UnionKeywordRecommender.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery; +using Microsoft.CodeAnalysis.CSharp.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.Completion.KeywordRecommenders; + +internal sealed class UnionKeywordRecommender() : AbstractSyntacticSingleKeywordRecommender(SyntaxKind.UnionKeyword) +{ + private static readonly ISet s_validModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.InternalKeyword, + SyntaxKind.PublicKeyword, + SyntaxKind.PrivateKeyword, + SyntaxKind.ProtectedKeyword, + SyntaxKind.UnsafeKeyword, + SyntaxKind.ReadOnlyKeyword, + SyntaxKind.FileKeyword, + }; + + protected override bool IsValidContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) + { + return + context.IsGlobalStatementContext || + context.IsTypeDeclarationContext( + validModifiers: s_validModifiers, + validTypeDeclarations: SyntaxKindSet.NonEnumTypeDeclarations, + canBePartial: true, + cancellationToken: cancellationToken); + } +} diff --git a/src/Features/CSharp/Portable/ConvertCast/CSharpConvertDirectCastToTryCastCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertCast/CSharpConvertDirectCastToTryCastCodeRefactoringProvider.cs index 207815140b66..2c8f11f7927d 100644 --- a/src/Features/CSharp/Portable/ConvertCast/CSharpConvertDirectCastToTryCastCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertCast/CSharpConvertDirectCastToTryCastCodeRefactoringProvider.cs @@ -4,14 +4,18 @@ using System.Composition; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.CodeAnalysis.CodeRefactorings; using Microsoft.CodeAnalysis.ConvertCast; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.CSharp.ConvertCast; +using static CSharpSyntaxTokens; +using static SyntaxFactory; + /// /// Refactor: /// var o = (object)1; @@ -20,15 +24,11 @@ namespace Microsoft.CodeAnalysis.CSharp.ConvertCast; /// var o = 1 as object; /// [ExportCodeRefactoringProvider(LanguageNames.CSharp, Name = PredefinedCodeRefactoringProviderNames.ConvertDirectCastToTryCast), Shared] -internal sealed partial class CSharpConvertDirectCastToTryCastCodeRefactoringProvider +[method: ImportingConstructor] +[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] +internal sealed partial class CSharpConvertDirectCastToTryCastCodeRefactoringProvider() : AbstractConvertCastCodeRefactoringProvider { - [ImportingConstructor] - [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")] - public CSharpConvertDirectCastToTryCastCodeRefactoringProvider() - { - } - protected override string GetTitle() => CSharpFeaturesResources.Change_to_as_expression; @@ -39,31 +39,39 @@ protected override TypeSyntax GetTypeNode(CastExpressionSyntax from) protected override BinaryExpressionSyntax ConvertExpression(CastExpressionSyntax castExpression, NullableContext nullableContext, bool isReferenceType) { - var typeNode = castExpression.Type; - var expression = castExpression.Expression; + var newTypeNode = castExpression.Type; // Cannot use nullable reference types in `as` expression // This check ensures we unwrap any nullables, e.g. // `(string?)null` -> `null as string` - if (typeNode is NullableTypeSyntax nullableType && isReferenceType) - typeNode = nullableType.ElementType; + if (newTypeNode is NullableTypeSyntax nullableType && isReferenceType) + newTypeNode = nullableType.ElementType; + + // (T)expr --> expr as T + + // 'expr' is moving to front. Move the existing leading trivia to it, and follow it with a before the 'as'. + var newExpression = castExpression.Expression + .WithLeadingTrivia(GetCommentTrivia(castExpression.CloseParenToken.TrailingTrivia)) + .WithPrependedLeadingTrivia(castExpression.GetLeadingTrivia()) + .WithTrailingTrivia(Space); + + // 'as' is in the middle. Ensure it has no elastic trivia, is followed by a space, and includes any random + // comments that might have come before the 'T' in the cast. + var newAsKeyword = AsKeyword + .WithoutTrivia() + .WithTrailingTrivia(Space) + .WithAppendedTrailingTrivia(GetCommentTrivia(castExpression.OpenParenToken.TrailingTrivia)); - // Trivia handling - // #0 ( #1 Type #2 ) #3 expr #4 - // #0 #3 expr as #1 Type #2 #4 - // If #1 is present a new line is added after "as" because of elastic trivia on "as" - // #3 is kept with the expression and moves - typeNode = typeNode.WithLeadingTrivia(castExpression.OpenParenToken.TrailingTrivia); - var middleTrivia = castExpression.CloseParenToken.TrailingTrivia.SkipInitialWhitespace(); - var newLeadingTrivia = castExpression.GetLeadingTrivia().AddRange(middleTrivia); - var newTrailingTrivia = typeNode.GetTrailingTrivia().WithoutLeadingBlankLines().AddRange(expression.GetTrailingTrivia().WithoutLeadingBlankLines()); - expression = expression.WithoutTrailingTrivia(); - typeNode = typeNode.WithoutTrailingTrivia(); + // 'T' is moving to the end. Move the existing trailing trivia to it, and ensure it has no leading trivia. + newTypeNode = newTypeNode + .WithoutTrivia() + .WithAppendedTrailingTrivia(GetCommentTrivia(newTypeNode.GetTrailingTrivia())) + .WithAppendedTrailingTrivia(castExpression.GetTrailingTrivia()); - var asExpression = BinaryExpression(SyntaxKind.AsExpression, expression, typeNode) - .WithLeadingTrivia(newLeadingTrivia) - .WithTrailingTrivia(newTrailingTrivia); + return BinaryExpression(SyntaxKind.AsExpression, newExpression, newAsKeyword, newTypeNode); - return asExpression; + // If there is a comment in the trivia, keep the whole list. Otherwise, return nothing. + static SyntaxTriviaList GetCommentTrivia(SyntaxTriviaList triviaList) + => triviaList.Any(t => t.IsRegularComment()) ? triviaList : []; } } diff --git a/src/Features/CSharp/Portable/GenerateType/CSharpGenerateTypeService.cs b/src/Features/CSharp/Portable/GenerateType/CSharpGenerateTypeService.cs index a22b05966073..b02d0a364aac 100644 --- a/src/Features/CSharp/Portable/GenerateType/CSharpGenerateTypeService.cs +++ b/src/Features/CSharp/Portable/GenerateType/CSharpGenerateTypeService.cs @@ -170,6 +170,13 @@ SyntaxKind.GenericName or return false; } + // Allow generate type in cref context () + if (simpleName.GetAncestor() != null) + { + generateTypeServiceStateOptions.NameOrMemberAccessExpression = simpleName; + return true; + } + // If we can guarantee it's a type only context, great. Otherwise, we may not want to // provide this here. var semanticModel = document.SemanticModel; diff --git a/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj b/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj index 7fc12382c97c..712a7ef95548 100644 --- a/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj +++ b/src/Features/CSharp/Portable/Microsoft.CodeAnalysis.CSharp.Features.csproj @@ -72,13 +72,6 @@ - - - - $(DefineConstants);FILE_BASED_PROGRAMS_SOURCE_PACKAGE_GRACEFUL_EXCEPTION diff --git a/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_MethodGroup.cs b/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_MethodGroup.cs index 5b2a83eecbef..2caeb8d3e650 100644 --- a/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_MethodGroup.cs +++ b/src/Features/CSharp/Portable/SignatureHelp/InvocationExpressionSignatureHelpProviderBase_MethodGroup.cs @@ -58,8 +58,8 @@ private static ImmutableArray GetAccessibleMethods( // SyntaxKind.SimpleMemberAccessExpression is for not imported types, e.g. "MyNamespace.MyClass.MyStaticMethod(...)" // SyntaxKind.PredefinedType is for built-in types, e.g. "string.Equals(...)" var includeInstance = throughExpression.Kind() is not (SyntaxKind.IdentifierName or SyntaxKind.SimpleMemberAccessExpression or SyntaxKind.PredefinedType) || - semanticModel.LookupSymbols(throughExpression.SpanStart, name: throughSymbol?.Name).Any(static s => s is not INamedTypeSymbol) || - (throughSymbol is not INamespaceOrTypeSymbol && semanticModel.LookupSymbols(throughExpression.SpanStart, container: throughSymbol?.ContainingType).Any(static s => s is not INamedTypeSymbol)); + throughSymbol is not INamespaceOrTypeSymbol || + semanticModel.LookupSymbols(throughExpression.SpanStart, name: throughSymbol?.Name).Any(static s => s is not INamedTypeSymbol); var includeStatic = throughSymbol is INamedTypeSymbol || (throughExpression.IsKind(SyntaxKind.IdentifierName) && diff --git a/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs b/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs index 0631779f5892..1a53dc013f63 100644 --- a/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs +++ b/src/Features/CSharp/Portable/Snippets/CSharpSnippetIdentifiers.cs @@ -25,6 +25,7 @@ internal static class CSharpSnippetIdentifiers public const string StaticIntMain = "sim"; public const string Struct = "struct"; public const string StaticVoidMain = "svm"; + public const string Unsafe = "unsafe"; public const string Using = "using"; public const string While = "while"; } diff --git a/src/Features/CSharp/Portable/Snippets/CSharpUnsafeSnippetProvider.cs b/src/Features/CSharp/Portable/Snippets/CSharpUnsafeSnippetProvider.cs new file mode 100644 index 000000000000..f177294ccb05 --- /dev/null +++ b/src/Features/CSharp/Portable/Snippets/CSharpUnsafeSnippetProvider.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Snippets; +using Microsoft.CodeAnalysis.Snippets.SnippetProviders; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CSharp.Snippets; + +[ExportSnippetProvider(nameof(ISnippetProvider), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpUnsafeSnippetProvider() : AbstractStatementSnippetProvider +{ + public override string Identifier => CSharpSnippetIdentifiers.Unsafe; + + public override string Description => CSharpFeaturesResources.unsafe_block; + + protected override Task GenerateSnippetTextChangeAsync(Document document, int position, CancellationToken cancellationToken) + => Task.FromResult(new TextChange(TextSpan.FromBounds(position, position), SyntaxFactory.UnsafeStatement().ToFullString())); + + protected override int GetTargetCaretPosition(UnsafeStatementSyntax unsafeStatement, SourceText sourceText) + => CSharpSnippetHelpers.GetTargetCaretPositionInBlock( + unsafeStatement, + static s => s.Block, + sourceText); + + protected override Task AddIndentationToDocumentAsync(Document document, UnsafeStatementSyntax unsafeStatement, CancellationToken cancellationToken) + => CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync( + document, + unsafeStatement, + static s => s.Block, + cancellationToken); +} diff --git a/src/Features/CSharp/Portable/Structure/Providers/InitializerExpressionStructureProvider.cs b/src/Features/CSharp/Portable/Structure/Providers/InitializerExpressionStructureProvider.cs index 44b29083f0f1..9f5866636666 100644 --- a/src/Features/CSharp/Portable/Structure/Providers/InitializerExpressionStructureProvider.cs +++ b/src/Features/CSharp/Portable/Structure/Providers/InitializerExpressionStructureProvider.cs @@ -55,14 +55,19 @@ protected override void CollectBlockSpans( // ... // } // - // The collapsed textspan should be from the > to the } + // The collapsed textspan should be from the { to the } // - // However, the hint span should be the entire object creation. + // The hint span is the containing statement for hover context. + + var containingStatement = node.FirstAncestorOrSelf(); + var hintSpan = containingStatement != null + ? containingStatement.Span + : node.Parent.Span; spans.Add(new BlockSpan( isCollapsible: true, - textSpan: TextSpan.FromBounds(previousToken.Span.End, node.Span.End), - hintSpan: node.Parent.Span, + textSpan: TextSpan.FromBounds(node.SpanStart, node.Span.End), + hintSpan: hintSpan, type: BlockTypes.Expression)); } } diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/.editorconfig b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/.editorconfig index 2a3f7addfd52..e10491047f54 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/.editorconfig +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/.editorconfig @@ -15,35 +15,4 @@ dotnet_diagnostic.CS1573.severity = none # As above, we need to specifically disable compiler warnings that we don't want to break downstream # builds -dotnet_diagnostic.IDE0005.severity = none - -# Disable nullability checks when targeting netstandard2.0 -dotnet_diagnostic.CS8774.severity = none # Member not null -dotnet_diagnostic.CS8775.severity = none # Member not null when -dotnet_diagnostic.CS8776.severity = none # Member not null bad member -dotnet_diagnostic.CS8777.severity = none # Parameter disallows null -dotnet_diagnostic.CS8778.severity = none # Const out of range checked -dotnet_diagnostic.CS8600.severity = none # Converting nullable to non-nullable -dotnet_diagnostic.CS8601.severity = none # Null reference assignment -dotnet_diagnostic.CS8602.severity = none # Null reference receiver -dotnet_diagnostic.CS8603.severity = none # Null reference return -dotnet_diagnostic.CS8604.severity = none # Null reference argument -dotnet_diagnostic.CS8605.severity = none # Unbox possible null -dotnet_diagnostic.CS8607.severity = none # Disallow null attribute forbids maybe null assignment -dotnet_diagnostic.CS8608.severity = none # Nullability mismatch in type on override -dotnet_diagnostic.CS8609.severity = none # Nullability mismatch in return type on override -dotnet_diagnostic.CS8610.severity = none # Nullability mismatch in parameter type on override -dotnet_diagnostic.CS8611.severity = none # Nullability mismatch in parameter type on partial -dotnet_diagnostic.CS8612.severity = none # Nullability mismatch in type on implicit implementation -dotnet_diagnostic.CS8613.severity = none # Nullability mismatch in return type on implicit implementation -dotnet_diagnostic.CS8614.severity = none # Nullability mismatch in parameter type on implicit implementation -dotnet_diagnostic.CS8615.severity = none # Nullability mismatch in type on explicit implementation -dotnet_diagnostic.CS8616.severity = none # Nullability mismatch in return type on explicit implementation -dotnet_diagnostic.CS8617.severity = none # Nullability mismatch in parameter type on explicit implementation -dotnet_diagnostic.CS8618.severity = none # Uninitialized non-nullable field -dotnet_diagnostic.CS8619.severity = none # Nullability mismatch in assignment -dotnet_diagnostic.CS8620.severity = none # Nullability mismatch in argument -dotnet_diagnostic.CS8621.severity = none # Nullability mismatch in return type of target delegate -dotnet_diagnostic.CS8622.severity = none # Nullability mismatch in parameter type of target delegate -dotnet_diagnostic.CS8624.severity = none # Nullability mismatch in argument for output -dotnet_diagnostic.CS8625.severity = none # Null as non-nullable \ No newline at end of file +dotnet_diagnostic.IDE0005.severity = none \ No newline at end of file diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx index 474f7236a2f7..c686b49b6ccc 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileBasedProgramsResources.resx @@ -1,17 +1,17 @@ - @@ -134,6 +134,10 @@ The property directive needs to have two parts separated by '=' like '#:property PropertyName=PropertyValue'. {Locked="#:property"} + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' + Static graph restore is not supported for file-based apps. Remove the '#:property'. {Locked="#:property"} @@ -169,7 +173,16 @@ Unrecognized directive '{0}'. {0} is the directive name like 'package' or 'sdk'. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs index 8457183e2cba..99e7ba172924 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/FileLevelDirectiveHelpers.cs @@ -36,7 +36,7 @@ public static SyntaxTokenParser CreateTokenizer(SourceText text) /// The latter is useful for dotnet run file.cs where if there are app directives after the first token, /// compiler reports anyway, so we speed up success scenarios by not parsing the whole file up front in the SDK CLI. /// - public static ImmutableArray FindDirectives(SourceFile sourceFile, bool reportAllErrors, ErrorReporter reportError) + public static ImmutableArray FindDirectives(SourceFile sourceFile, bool reportAllErrors, ErrorReporter errorReporter) { var builder = ImmutableArray.CreateBuilder(); var tokenizer = CreateTokenizer(sourceFile.Text); @@ -44,7 +44,7 @@ public static ImmutableArray FindDirectives(SourceFile sourceFi var result = tokenizer.ParseLeadingTrivia(); var triviaList = result.Token.LeadingTrivia; - FindLeadingDirectives(sourceFile, triviaList, reportError, builder); + FindLeadingDirectives(sourceFile, triviaList, errorReporter, builder); // In conversion mode, we want to report errors for any invalid directives in the rest of the file // so users don't end up with invalid directives in the converted project. @@ -73,7 +73,7 @@ void ReportErrorFor(SyntaxTrivia trivia) { if (trivia.ContainsDiagnostics && trivia.IsKind(SyntaxKind.IgnoredDirectiveTrivia)) { - reportError(sourceFile, trivia.Span, FileBasedProgramsResources.CannotConvertDirective); + errorReporter(sourceFile.Text, sourceFile.Path, trivia.Span, FileBasedProgramsResources.CannotConvertDirective); } } @@ -86,11 +86,9 @@ void ReportErrorFor(SyntaxTrivia trivia) public static void FindLeadingDirectives( SourceFile sourceFile, SyntaxTriviaList triviaList, - ErrorReporter reportError, + ErrorReporter errorReporter, ImmutableArray.Builder? builder) { - Debug.Assert(triviaList.Span.Start == 0); - var deduplicated = new Dictionary(NamedDirectiveComparer.Instance); TextSpan previousWhiteSpaceSpan = default; @@ -117,6 +115,7 @@ public static void FindLeadingDirectives( var whiteSpace = GetWhiteSpaceInfo(triviaList, index); var info = new CSharpDirective.ParseInfo { + SourceFile = sourceFile, Span = span, LeadingWhiteSpace = whiteSpace.Leading, TrailingWhiteSpace = whiteSpace.Trailing, @@ -140,12 +139,12 @@ public static void FindLeadingDirectives( { Info = new() { + SourceFile = sourceFile, Span = span, LeadingWhiteSpace = whiteSpace.Leading, TrailingWhiteSpace = whiteSpace.Trailing, }, - ReportError = reportError, - SourceFile = sourceFile, + ErrorReporter = errorReporter, DirectiveKind = name, DirectiveText = value, }; @@ -153,17 +152,16 @@ public static void FindLeadingDirectives( // Block quotes now so we can later support quoted values without a breaking change. https://github.com/dotnet/sdk/issues/49367 if (value.Contains('"')) { - reportError(sourceFile, context.Info.Span, FileBasedProgramsResources.QuoteInDirective); + context.ReportError(FileBasedProgramsResources.QuoteInDirective); } if (CSharpDirective.Parse(context) is { } directive) { // If the directive is already present, report an error. - if (deduplicated.ContainsKey(directive)) + if (deduplicated.TryGetValue(directive, out var existingDirective)) { - var existingDirective = deduplicated[directive]; var typeAndName = $"#:{existingDirective.GetType().Name.ToLowerInvariant()} {existingDirective.Name}"; - reportError(sourceFile, directive.Info.Span, string.Format(FileBasedProgramsResources.DuplicateDirective, typeAndName)); + context.ReportError(directive.Info.Span, string.Format(FileBasedProgramsResources.DuplicateDirective, typeAndName)); } else { @@ -232,11 +230,6 @@ public static SourceFile Load(string filePath) return new SourceFile(filePath, SourceText.From(stream, encoding: null)); } - public SourceFile WithText(SourceText newText) - { - return new SourceFile(Path, newText); - } - public void Save() { using var stream = File.Open(Path, FileMode.Create, FileAccess.Write); @@ -246,15 +239,9 @@ public void Save() Text.Write(writer); } - public FileLinePositionSpan GetFileLinePositionSpan(TextSpan span) - { - return new FileLinePositionSpan(Path, Text.Lines.GetLinePositionSpan(span)); - } - public string GetLocationString(TextSpan span) { - var positionSpan = GetFileLinePositionSpan(span); - return $"{positionSpan.Path}({positionSpan.StartLinePosition.Line + 1})"; + return $"{Path}({Text.Lines.GetLinePositionSpan(span).Start.Line + 1})"; } } @@ -283,6 +270,7 @@ internal abstract class CSharpDirective(in CSharpDirective.ParseInfo info) public readonly struct ParseInfo { + public required SourceFile SourceFile { get; init; } /// /// Span of the full line including the trailing line break. /// @@ -294,10 +282,15 @@ public readonly struct ParseInfo public readonly struct ParseContext { public required ParseInfo Info { get; init; } - public required ErrorReporter ReportError { get; init; } - public required SourceFile SourceFile { get; init; } + public required ErrorReporter ErrorReporter { get; init; } public required string DirectiveKind { get; init; } public required string DirectiveText { get; init; } + + public void ReportError(string message) + => ErrorReporter(Info.SourceFile.Text, Info.SourceFile.Path, Info.Span, message); + + public void ReportError(TextSpan span, string message) + => ErrorReporter(Info.SourceFile.Text, Info.SourceFile.Path, span, message); } public static Named? Parse(in ParseContext context) @@ -308,10 +301,11 @@ public readonly struct ParseContext case "property": return Property.Parse(context); case "package": return Package.Parse(context); case "project": return Project.Parse(context); + case "include" or "exclude": return IncludeOrExclude.Parse(context); default: - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.UnrecognizedDirective, context.DirectiveKind)); + context.ReportError(string.Format(FileBasedProgramsResources.UnrecognizedDirective, context.DirectiveKind)); return null; - }; + } } private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, char separator) @@ -322,14 +316,14 @@ private static (string, string?)? ParseOptionalTwoParts(in ParseContext context, string directiveKind = context.DirectiveKind; if (firstPart.IsWhiteSpace()) { - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); return null; } // If the name contains characters that resemble separators, report an error to avoid any confusion. if (Patterns.DisallowedNameCharacters.Match(context.DirectiveText, beginning: 0, length: firstPart.Length).Success) { - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.InvalidDirectiveName, directiveKind, separator)); + context.ReportError(string.Format(FileBasedProgramsResources.InvalidDirectiveName, directiveKind, separator)); return null; } @@ -405,7 +399,7 @@ public sealed class Property(in ParseInfo info) : Named(info) if (propertyValue is null) { - context.ReportError(context.SourceFile, context.Info.Span, FileBasedProgramsResources.PropertyDirectiveMissingParts); + context.ReportError(FileBasedProgramsResources.PropertyDirectiveMissingParts); return null; } @@ -415,14 +409,14 @@ public sealed class Property(in ParseInfo info) : Named(info) } catch (XmlException ex) { - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, ex.Message)); + context.ReportError(string.Format(FileBasedProgramsResources.PropertyDirectiveInvalidName, ex.Message)); return null; } if (propertyName.Equals("RestoreUseStaticGraphEvaluation", StringComparison.OrdinalIgnoreCase) && MSBuildUtilities.ConvertStringToBool(propertyValue)) { - context.ReportError(context.SourceFile, context.Info.Span, FileBasedProgramsResources.StaticGraphRestoreNotSupported); + context.ReportError(FileBasedProgramsResources.StaticGraphRestoreNotSupported); } return new Property(context.Info) @@ -494,8 +488,7 @@ public Project(in ParseInfo info, string name) : base(info) var directiveText = context.DirectiveText; if (directiveText.IsWhiteSpace()) { - string directiveKind = context.DirectiveKind; - context.ReportError(context.SourceFile, context.Info.Span, string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, context.DirectiveKind)); return null; } @@ -533,14 +526,15 @@ public Project WithName(string name, NameKind kind) /// /// If the directive points to a directory, returns a new directive pointing to the corresponding project file. /// - public Project EnsureProjectFilePath(SourceFile sourceFile, ErrorReporter reportError) + public Project EnsureProjectFilePath(ErrorReporter errorReporter) { var resolvedName = Name; + var sourcePath = Info.SourceFile.Path; // If the path is a directory like '../lib', transform it to a project file path like '../lib/lib.csproj'. // Also normalize backslashes to forward slashes to ensure the directive works on all platforms. - var sourceDirectory = Path.GetDirectoryName(sourceFile.Path) - ?? throw new InvalidOperationException($"Source file path '{sourceFile.Path}' does not have a containing directory."); + var sourceDirectory = Path.GetDirectoryName(sourcePath) + ?? throw new InvalidOperationException($"Source file path '{sourcePath}' does not have a containing directory."); var resolvedProjectPath = Path.Combine(sourceDirectory, resolvedName.Replace('\\', '/')); if (Directory.Exists(resolvedProjectPath)) @@ -554,20 +548,220 @@ public Project EnsureProjectFilePath(SourceFile sourceFile, ErrorReporter report } else { - reportError(sourceFile, Info.Span, string.Format(FileBasedProgramsResources.InvalidProjectDirective, error)); + ReportError(string.Format(FileBasedProgramsResources.InvalidProjectDirective, error)); } } else if (!File.Exists(resolvedProjectPath)) { - reportError(sourceFile, Info.Span, - string.Format(FileBasedProgramsResources.InvalidProjectDirective, string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, resolvedProjectPath))); + ReportError(string.Format(FileBasedProgramsResources.InvalidProjectDirective, string.Format(FileBasedProgramsResources.CouldNotFindProjectOrDirectory, resolvedProjectPath))); } return WithName(resolvedName, NameKind.ProjectFilePath); + + void ReportError(string message) + => errorReporter(Info.SourceFile.Text, sourcePath, Info.Span, message); } public override string ToString() => $"#:project {Name}"; } + + public enum IncludeOrExcludeKind + { + Include, + Exclude, + } + + /// + /// #:include or #:exclude directive. + /// + public sealed class IncludeOrExclude(in ParseInfo info) : Named(info) + { + public const string ExperimentalFileBasedProgramEnableIncludeDirective = nameof(ExperimentalFileBasedProgramEnableIncludeDirective); + public const string ExperimentalFileBasedProgramEnableExcludeDirective = nameof(ExperimentalFileBasedProgramEnableExcludeDirective); + public const string ExperimentalFileBasedProgramEnableTransitiveDirectives = nameof(ExperimentalFileBasedProgramEnableTransitiveDirectives); + public const string ExperimentalFileBasedProgramEnableItemMapping = nameof(ExperimentalFileBasedProgramEnableItemMapping); + + public const string MappingPropertyName = "FileBasedProgramsItemMapping"; + + public static string DefaultMappingString => ".cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content"; + + public static ImmutableArray<(string Extension, string ItemType)> DefaultMapping + { + get + { + if (field.IsDefault) + { + field = + [ + (".cs", "Compile"), + (".resx", "EmbeddedResource"), + (".json", "None"), + (".razor", "Content"), + ]; + } + + return field; + } + } + + /// + /// Preserved across calls, i.e., + /// this is the original directive text as entered by the user. + /// + public required string OriginalName { get; init; } + + public required IncludeOrExcludeKind Kind { get; init; } + + public string? ItemType { get; init; } + + public static new IncludeOrExclude? Parse(in ParseContext context) + { + var directiveText = context.DirectiveText; + if (directiveText.IsWhiteSpace()) + { + string directiveKind = context.DirectiveKind; + context.ReportError(string.Format(FileBasedProgramsResources.MissingDirectiveName, directiveKind)); + return null; + } + + return new IncludeOrExclude(context.Info) + { + OriginalName = directiveText, + Name = directiveText, + Kind = KindFromString(context.DirectiveKind), + }; + } + + /// + /// See . + /// + public IncludeOrExclude WithDeterminedItemType(ErrorReporter reportError, ImmutableArray<(string Extension, string ItemType)> mapping) + { + Debug.Assert(ItemType is null); + + string? itemType = null; + foreach (var entry in mapping) + { + if (Name.EndsWith(entry.Extension, StringComparison.OrdinalIgnoreCase)) + { + itemType = entry.ItemType; + break; + } + } + + if (itemType is null) + { + reportError(Info.SourceFile.Text, Info.SourceFile.Path, Info.Span, + string.Format(FileBasedProgramsResources.IncludeOrExcludeDirectiveUnknownFileType, + $"#:{KindToString()}", + string.Join(", ", mapping.Select(static e => e.Extension)))); + return this; + } + + return new IncludeOrExclude(Info) + { + OriginalName = OriginalName, + Name = Name, + Kind = Kind, + ItemType = itemType, + }; + } + + public IncludeOrExclude WithName(string name) + { + if (Name == name) + { + return this; + } + + return new IncludeOrExclude(Info) + { + OriginalName = OriginalName, + Name = name, + Kind = Kind, + ItemType = ItemType, + }; + } + + private static IncludeOrExcludeKind KindFromString(string kind) + { + return kind switch + { + "include" => IncludeOrExcludeKind.Include, + "exclude" => IncludeOrExcludeKind.Exclude, + _ => throw new InvalidOperationException($"Unexpected include/exclude directive kind '{kind}'."), + }; + } + + public string KindToString() + { + return Kind switch + { + IncludeOrExcludeKind.Include => "include", + IncludeOrExcludeKind.Exclude => "exclude", + _ => throw new InvalidOperationException($"Unexpected {nameof(IncludeOrExcludeKind)} value '{Kind}'."), + }; + } + + public string KindToMSBuildString() + { + return Kind switch + { + IncludeOrExcludeKind.Include => "Include", + IncludeOrExcludeKind.Exclude => "Remove", + _ => throw new InvalidOperationException($"Unexpected {nameof(IncludeOrExcludeKind)} value '{Kind}'."), + }; + } + + public override string ToString() => $"#:{KindToString()} {Name}"; + + /// + /// Parses a in the format .protobuf=Protobuf;.cshtml=Content. + /// Should come from MSBuild property with name . + /// + public static ImmutableArray<(string Extension, string ItemType)> ParseMapping( + string value, + SourceFile sourceFile, + ErrorReporter errorReporter) + { + var pairs = value.Split(';'); + + var builder = ImmutableArray.CreateBuilder<(string Extension, string ItemType)>(pairs.Length); + + foreach (var pair in pairs) + { + var parts = pair.Split('='); + + if (parts.Length != 2) + { + ReportError(string.Format(FileBasedProgramsResources.InvalidIncludeExcludeMappingEntry, pair)); + continue; + } + + var extension = parts[0].Trim(); + var itemType = parts[1].Trim(); + + if (extension is not ['.', _, ..]) + { + ReportError(string.Format(FileBasedProgramsResources.InvalidIncludeExcludeMappingExtension, extension, pair)); + continue; + } + + if (itemType.IsWhiteSpace()) + { + ReportError(string.Format(FileBasedProgramsResources.InvalidIncludeExcludeMappingItemType, itemType, pair)); + continue; + } + + builder.Add((extension, itemType)); + } + + return builder.DrainToImmutable(); + + void ReportError(string message) + => errorReporter(sourceFile.Text, sourceFile.Path, default, message); + } + } } /// @@ -618,25 +812,25 @@ public readonly struct Position } } -internal delegate void ErrorReporter(SourceFile sourceFile, TextSpan textSpan, string message); +internal delegate void ErrorReporter(SourceText text, string path, TextSpan textSpan, string message, Exception? innerException = null); internal static partial class ErrorReporters { public static readonly ErrorReporter IgnoringReporter = - static (_, _, _) => { }; + static (_, _, _, _, _) => { }; public static ErrorReporter CreateCollectingReporter(out ImmutableArray.Builder builder) { var capturedBuilder = builder = ImmutableArray.CreateBuilder(); - return (sourceFile, textSpan, message) => + return (text, path, textSpan, message, _) => capturedBuilder.Add(new SimpleDiagnostic { Location = new SimpleDiagnostic.Position() { - Path = sourceFile.Path, + Path = path, TextSpan = textSpan, - Span = sourceFile.GetFileLinePositionSpan(textSpan).Span + Span = text.Lines.GetLinePositionSpan(textSpan) }, Message = message }); diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf index 37292a467a31..fc0195eeec11 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.cs.xlf @@ -27,16 +27,31 @@ Duplicitní direktivy nejsou podporovány: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Nelze určit cestu k dočasnému adresáři. Zvažte konfiguraci proměnné prostředí TEMP v systému Windows nebo místní datové složky aplikace v systému Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Nerozpoznaná přípona souboru v direktivě {0}. V současné době jsou rozpoznávány pouze tyto přípony: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Direktiva by měla obsahovat název bez speciálních znaků a volitelnou hodnotu oddělenou znakem {1}, například #:{0} Název{1}Hodnota. {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + Každá položka ve vlastnosti MSBuild FileBasedProgramsItemMapping musí mít dvě části oddělené znakem „=“. Položka {0} je neplatná. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Každá položka ve vlastnosti MSBuild FileBasedProgramsItemMapping musí být mapována z neprázdné přípony souboru začínající na „.“. Přípona {0} v položce {1} je neplatná. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + Každá položka ve vlastnosti MSBuild FileBasedProgramsItemMapping musí být mapována na neprázdný typ položky. Typ položky {0} v položce {1} je neplatný. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} Direktiva #:project je neplatná: {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf index 991dcca846e4..b76d1b5f9782 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.de.xlf @@ -27,16 +27,31 @@ Doppelte Anweisungen werden nicht unterstützt: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Ein temporärer Verzeichnispfad kann nicht ermittelt werden. Erwägen Sie, die TEMP-Umgebungsvariable unter Windows oder den lokalen App-Datenordner unter Unix zu konfigurieren. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Unbekannte Dateierweiterung in der „{0}“-Anweisung. Derzeit werden nur diese Erweiterungen erkannt: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Die Anweisung sollte einen Namen ohne Sonderzeichen und einen optionalen Wert enthalten, die durch „{1}“ getrennt sind, wie „#:{0} Name{1}Wert“. {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + Jeder Eintrag in der MSBuild-Eigenschaft „FileBasedProgramsItemMapping“ muss aus zwei durch „=“ getrennten Teilen bestehen. Der Eintrag „{0}“ ist ungültig. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Jeder Eintrag in der MSBuild-Eigenschaft „FileBasedProgramsItemMapping“ muss einer nicht leeren Dateierweiterung zugeordnet sein, die mit „.“ beginnt. Die Erweiterung „{0}“ im Eintrag „{1}“ ist ungültig. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + Jeder Eintrag in der MSBuild-Eigenschaft „FileBasedProgramsItemMapping“ muss einem nicht leeren Elementtyp zugeordnet sein. Der Elementtyp „{0}“ im Eintrag „{1}“ ist ungültig. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} Die Anweisung „#:p roject“ ist ungültig: {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf index 2983aa595df6..84a6d5d8fe79 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.es.xlf @@ -27,16 +27,31 @@ No se admiten directivas duplicadas: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - No se puede determinar una ruta de acceso temporal al directorio. Considere la posibilidad de configurar la variable de entorno TEMP en Windows o la carpeta de datos de la aplicación local en Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Extensión de archivo no reconocida en la directiva ''{0}. Actualmente solo se reconocen estas extensiones: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. La directiva debe contener un nombre sin caracteres especiales y un valor opcional separado por "{1}" como "#:{0} Nombre{1}Valor". {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + Cada entrada de la propiedad MSBuild ''FileBasedProgramsItemMapping'' debe tener dos partes separadas por '='. La entrada ''{0}'' no es válida. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Cada entrada de la propiedad MSBuild ''FileBasedProgramsItemMapping'' debe asignarse desde una extensión de archivo que no esté vacía a partir de '.'. La extensión ''{0}'' de la entrada ''{1}'' no es válida. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + Cada entrada de la propiedad MSBuild ''FileBasedProgramsItemMapping'' debe asignarse a un tipo de elemento no vacío. El tipo de elemento ''{0}'' de la entrada ''{1}'' no es válido. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} La directiva "#:project" no es válida: {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf index 1fabce52be8a..3d647ae12c89 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.fr.xlf @@ -27,16 +27,31 @@ Les directives dupliquées ne sont pas prises en charge : {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Impossible de déterminer un chemin d’accès pour le répertoire temporaire. Nous vous recommandons de configurer la variable d’environnement TEMP sous Windows ou le dossier des données d’application locale sous Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Extension de fichier non reconnue dans la directive « {0} ». Seules ces extensions sont actuellement reconnues : {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. La directive dans doit contenir un nom sans caractères spéciaux et une valeur facultative séparée par « {1} » comme « # :{0} Nom{1}Valeur ». {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + Chaque entrée de la propriété MSBuild « FileBasedProgramsItemMapping » doit comporter deux parties séparées par « = ». L’entrée « {0} » est invalide. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Chaque entrée de la propriété MSBuild « FileBasedProgramsItemMapping » doit correspondre à une extension de fichier non vide commençant par « . ». L’extension « {0} » dans l’entrée « {1} » est invalide. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + Chaque entrée de la propriété MSBuild « FileBasedProgramsItemMapping » doit correspondre à un type d’élément non vide. Le type d’élément « {0} » dans l’entrée « {1} » est invalide. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} La directive « #:project » n’est pas valide : {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf index 93fdb8209fd1..566e9deea623 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.it.xlf @@ -27,16 +27,31 @@ Le direttive duplicate non supportate: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Non è possibile determinare un percorso per la directory temporanea. Considerare la configurazione della variabile di ambiente TEMP in Windows o della cartella dei dati locali dell'app in Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Estensione file non riconosciuta nella direttiva "{0}". Sono riconosciute solo queste estensioni: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. La direttiva deve contenere un nome senza caratteri speciali e un valore facoltativo delimitato da '{1}' come '#:{0}Nome {1}Valore'. {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + Ogni voce nella proprietà MSBuild "FileBasedProgramsItemMapping" deve avere due parti separate da "=". La voce "{0}" non è valida. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Ogni voce nella proprietà MSBuild "FileBasedProgramsItemMapping" deve essere mappata da un'estensione di file non vuota che inizia con ".". L'estensione "{0}" nella voce "{1}" non è valida. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + Ogni voce nella proprietà MSBuild "FileBasedProgramsItemMapping" deve essere mappata a un tipo di elemento non vuoto. Il tipo di elemento "{0}" nella voce "{1}" non è valido. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} La direttiva '#:project' non è valida: {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf index f76d2b528243..6fbc4b90b118 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ja.xlf @@ -27,16 +27,31 @@ 重複するディレクティブはサポートされていません: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - 一時ディレクトリ パスを特定できません。Windows で TEMP 環境変数を構成するか、Unix でローカル アプリ データ フォルダーを構成することを検討してください。 - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + '{0}' ディレクティブ内の認識されないファイル拡張子。現在認識されている拡張子は次のとおりです: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. ディレクティブには、特殊文字を含まない名前と、'#:{0} Name{1}Value' などの '{1}' で区切られた省略可能な値を含める必要があります。 {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild プロパティの各エントリには、'=' で区切られた 2 つの部分が必要です。エントリ '{0}' が無効です。 + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild プロパティの各エントリは、空でないアイテムの種類にマップする必要があります。エントリ '{1}' のアイテムの種類 '{0}' が無効です。 + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} '#:p roject' ディレクティブが無効です: {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf index afdd17c8f433..8754f08de51c 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ko.xlf @@ -27,16 +27,31 @@ 중복 지시문은 지원되지 않습니다. {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - 임시 디렉터리 경로를 확인할 수 없습니다. Windows에서는 TEMP 환경 변수를, Unix에서는 로컬 앱 데이터 폴더를 설정하는 것이 좋습니다. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + '{0}' 지시문에서 인식할 수 없는 파일 확장자입니다. 현재 인식되는 확장자는 다음과 같습니다. {1}. + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. 지시문에는 특수 문자가 없는 이름과 '#:{0} 이름{1}값'과 같이 '{1}'(으)로 구분된 선택적 값이 포함되어야 합니다. {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild 속성의 각 항목은 '='로 구분된 두 부분으로 구성되어야 합니다. '{0}' 항목이 잘못되었습니다. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild 속성의 각 항목은 '.'로 시작하는 비어 있지 않은 파일 확장명에 매핑되어야 합니다. '{0}' 항목의 확장명 '{1}'이(가) 잘못되었습니다. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild 속성의 각 항목은 비어 있지 않은 항목 종류에 매핑되어야 합니다. '{0}' 항목의 항목 종류 '{1}'이(가) 잘못되었습니다. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} '#:p roject' 지시문이 잘못되었습니다. {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf index 5e91ebbb30cf..74c0f1593501 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pl.xlf @@ -27,16 +27,31 @@ Zduplikowane dyrektywy nie są obsługiwane: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Nie można określić tymczasowej ścieżki katalogu. Rozważ skonfigurowanie zmiennej środowiskowej TEMP w systemie Windows lub folderze danych aplikacji lokalnej w systemie Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Nierozpoznane rozszerzenie pliku w dyrektywie „{0}”. Obecnie rozpoznawane są tylko te rozszerzenia: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Dyrektywa powinna zawierać nazwę bez znaków specjalnych i opcjonalną wartość rozdzieloną znakiem "{1}#:{0} Name{1}Value". {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + Każdy wpis we właściwości MSBuild „FileBasedProgramsItemMapping” musi składać się z dwóch części oddzielonych znakiem „=”. Wpis „{0}” jest nieprawidłowy. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Każdy wpis we właściwości MSBuild „FileBasedProgramsItemMapping” musi mapować niepuste rozszerzenie pliku zaczynające się od „.”. Rozszerzenie „{0}” we wpisie „{1}” jest nieprawidłowe. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + Każdy wpis w właściwości MSBuild „FileBasedProgramsItemMapping” musi mapować na niepusty typ elementu. Typ elementu „{0}” we wpisie „{1}” jest nieprawidłowy. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} Dyrektywa „#:project” jest nieprawidłowa: {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf index f44fda05d927..2c5477966a90 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.pt-BR.xlf @@ -27,16 +27,31 @@ Diretivas duplicadas não são suportadas:{0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Não é possível determinar um caminho de diretório temporário. Considere configurar a variável de ambiente TEMP no Windows ou a pasta de dados do aplicativo local no Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Extensão de arquivo não reconhecida na diretiva '{0}'. Somente estas extensões são reconhecidas atualmente: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. A diretiva deve conter um nome sem caracteres especiais e um valor opcional separado por '{1}' como '#:{0} Nome{1}Valor'. {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + Cada entrada na propriedade MSBuild 'FileBasedProgramsItemMapping' deve ter duas partes separadas por '='. A entrada '{0}' é inválida. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Cada entrada na propriedade MSBuild 'FileBasedProgramsItemMapping' deve mapear a partir de uma extensão de arquivo não vazia que comece com '.'. A extensão '{0}' na entrada '{1}' é inválida. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + Cada entrada na propriedade MSBuild 'FileBasedProgramsItemMapping' deve mapear para um tipo de item não vazio. O tipo de item '{0}' na entrada '{1}' é inválido. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} A diretiva '#:project' é inválida:{0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf index 5e9af0ed594f..c383d8e60274 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.ru.xlf @@ -27,16 +27,31 @@ Повторяющиеся директивы не поддерживаются: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Не удалось определить путь к временному каталогу. Рассмотрите возможность настроить переменную среды TEMP в Windows или папку локальных данных приложений в Unix. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + Нераспознанное расширение файла в директиве "{0}". В настоящее время распознаются только следующие расширения: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Директива должна содержать имя без специальных символов и необязательное значение, разделенные символом-разделителем "{1}", например "#:{0} Имя{1}Значение". {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + Каждая запись в свойстве MSBuild "FileBasedProgramsItemMapping" должна содержать две части, разделенные "=". Запись "{0}" недопустима. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Каждая запись в свойстве MSBuild "FileBasedProgramsItemMapping" должна сопоставляться с непустым расширением файла, начинающимся с ".". Расширение "{0}" в записи "{1}" недопустимо. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + Каждая запись в свойстве MSBuild "FileBasedProgramsItemMapping" должна сопоставляться с непустым типом элемента. Тип элемента "{0}" в записи "{1}" недопустим. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} Недопустимая директива "#:project": {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf index 93c4d39ae84d..68b291084dbc 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.tr.xlf @@ -27,16 +27,31 @@ Yinelenen yönergeler desteklenmez: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - Geçici dizin yolu saptanamıyor. Windows'da TEMP ortam değişkenini veya Unix'te yerel uygulama verileri klasörünü yapılandırmayı göz önünde bulundurun. - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + '{0}' yönergesinde tanınmayan dosya uzantısı var. Şu anda yalnızca şu uzantılar tanınıyor: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. Yönerge, özel karakterler içermeyen bir ad ve ‘#:{0} Ad{1}Değer’ gibi '{1}' ile ayrılmış isteğe bağlı bir değer içermelidir. {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild özelliğindeki her girdi, '=' ile ayrılmış iki bölümden oluşmalıdır. '{0}' girdisi geçersizdir. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild özelliğindeki her girdi, '.' ile başlayan boş olmayan bir dosya uzantısına karşılık gelmelidir. '{1}' girdisindeki '{0}' uzantısı geçersizdir. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild özelliğindeki her girdi, boş olmayan bir öğe türüne karşılık gelmelidir. '{1}' girdisindeki '{0}' öğe türü geçersizdir. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} ‘#:project’ yönergesi geçersizdir: {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf index 27b469f6394b..31dce0ac6e94 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hans.xlf @@ -27,16 +27,31 @@ 不支持重复指令: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - 无法确定临时目录路径。请考虑在 Windows 上配置 TEMP 环境变量,或在 Unix 上配置本地应用数据文件夹。 - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + '{0}' 指令中的文件扩展名无法识别。当前仅识别以下扩展名: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. 该指令应包含一个不带特殊字符的名称,以及一个以 '#:{0} Name{1}Value' 等 ‘{1}’ 分隔的可选值。 {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild 属性中的每个条目必须包含由 '=' 分隔的两部分。条目 '{0}' 无效。 + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild 属性中的每个条目必须映射到非空项类型。条目 '{1}' 中的项类型 '{0}' 无效。 + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} '#:project' 指令无效: {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf index f5a7d9f89eea..5c9f29928cfb 100644 --- a/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf +++ b/src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms/xlf/FileBasedProgramsResources.zh-Hant.xlf @@ -27,16 +27,31 @@ 不支援重複的指示詞: {0} {0} is the directive type and name. - - Unable to determine a temporary directory path. Consider configuring the TEMP environment variable on Windows or local app data folder on Unix. - 無法判斷暫存 目錄路徑。考慮在 Windows 上或 Unix 上的本機應用程式資料資料資料夾上設定 TEMP 環境變數。 - + + Unrecognized file extension in the '{0}' directive. Only these extensions are currently recognized: {1} + '{0}' 指示詞中無法辨識的副檔名。目前僅能識別這些副檔名: {1} + {0} is the directive - '#:include' or '#:exclude'. {1} is a comma-separated list of file extensions, like: '.cs', '.resx' The directive should contain a name without special characters and an optional value separated by '{1}' like '#:{0} Name{1}Value'. 指示詞應包含不含特殊字元的名稱,以及 '{1}' 分隔的選用值,例如 '#:{0} Name{1}Value'。 {0} is the directive type like 'package' or 'sdk'. {1} is the expected separator like '@' or '='. + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must have two parts separated by '='. The entry '{0}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild 屬性中的每個項目都必須有兩個部分,以 '=' 區隔。項目 '{0}' 無效。 + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="="} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map from a non-empty file extension starting with '.'. The extension '{0}' in entry '{1}' is invalid. + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"}{Locked="."} + + + Each entry in 'FileBasedProgramsItemMapping' MSBuild property must map to a non-empty item type. The item type '{0}' in entry '{1}' is invalid. + 'FileBasedProgramsItemMapping' MSBuild 屬性中的每個項目都必須與非空白的項目類型對應。項目 '{1}' 中的項目類型 '{0}' 無效。 + {Locked="FileBasedProgramsItemMapping"}{Locked="MSBuild"} + The '#:project' directive is invalid: {0} '#:project' 指示詞無效: {0} diff --git a/src/Features/CSharp/Portable/SyncedSource/commitid.txt b/src/Features/CSharp/Portable/SyncedSource/commitid.txt new file mode 100644 index 000000000000..dea4abdde72f --- /dev/null +++ b/src/Features/CSharp/Portable/SyncedSource/commitid.txt @@ -0,0 +1 @@ +b6ecfca4772c223907a0fe13b0ab944a8e197d53 \ No newline at end of file diff --git a/src/Features/CSharp/Portable/TypeHierarchy/CSharpTypeHierarchyService.cs b/src/Features/CSharp/Portable/TypeHierarchy/CSharpTypeHierarchyService.cs new file mode 100644 index 000000000000..86334a655d6d --- /dev/null +++ b/src/Features/CSharp/Portable/TypeHierarchy/CSharpTypeHierarchyService.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.TypeHierarchy; + +namespace Microsoft.CodeAnalysis.CSharp.TypeHierarchy; + +[ExportLanguageService(typeof(ITypeHierarchyService), LanguageNames.CSharp), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CSharpTypeHierarchyService() : AbstractTypeHierarchyService; diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf index ed96a6f10efc..18472fc06bec 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.cs.xlf @@ -32,6 +32,11 @@ Přidá odkaz na balíček NuGet. + + Adds a file reference. + Přidá odkaz na soubor. + + Adds a project reference. Přidá odkaz na projekt. @@ -247,6 +252,11 @@ Globální direktivy using + + path + cesta + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible Nastavit privátní pole jako jenom pro čtení, kde je to možné @@ -517,6 +527,11 @@ příkaz fixed {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + nebezpečný blok + + using declaration deklarace using diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf index 3e59785c9a1e..c4143c22a0ba 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.de.xlf @@ -32,6 +32,11 @@ Fügt einen NuGet-Paketverweis hinzu. + + Adds a file reference. + Fügt einen Dateiverweis hinzu. + + Adds a project reference. Fügt einen Projektverweis hinzu. @@ -247,6 +252,11 @@ Globale "Using"-Anweisungen + + path + Pfad + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible Private Felder nach Möglichkeit als schreibgeschützt festlegen @@ -517,6 +527,11 @@ fixed-Anweisung {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + unsicherer Block + + using declaration using-Deklaration diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf index e41f6e726e2e..074b8377704e 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.es.xlf @@ -32,6 +32,11 @@ Agrega una referencia a un paquete NuGet. + + Adds a file reference. + Añade una referencia de archivo. + + Adds a project reference. Agregar una referencia a un proyecto. @@ -247,6 +252,11 @@ Directivas "using" globales + + path + ruta de acceso + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible Cuando sea posible, hacer que los cambios privados sean solo de lectura @@ -517,6 +527,11 @@ instrucción "fixed" {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + bloque no seguro + + using declaration declaración using diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf index 266c0685cc4d..6e205e418399 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.fr.xlf @@ -32,6 +32,11 @@ Ajouter une référence de package NuGet. + + Adds a file reference. + Ajoute une référence de fichier. + + Adds a project reference. Ajoute une référence de projet. @@ -247,6 +252,11 @@ Directives « using » globales + + path + chemin d’accès + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible Configurer les champs privés en lecture seule quand cela est possible @@ -517,6 +527,11 @@ instruction fixed {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + bloc unsafe + + using declaration déclaration using diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf index 481ab089d8fa..e1bf2d04679d 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.it.xlf @@ -32,6 +32,11 @@ Aggiunge un riferimento al pacchetto NuGet. + + Adds a file reference. + Aggiunge un riferimento a un file. + + Adds a project reference. Aggiungi un riferimento al progetto. @@ -247,6 +252,11 @@ Direttive globali 'using' + + path + percorso + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible Imposta i campi privati come di sola lettura quando possibile @@ -517,6 +527,11 @@ istruzione fixed {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + blocco non gestito + + using declaration dichiarazione using diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf index eff255665977..e0c6b22c3174 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ja.xlf @@ -32,6 +32,11 @@ NuGet パッケージ参照を追加します。 + + Adds a file reference. + ファイル参照を追加します。 + + Adds a project reference. プロジェクト参照を追加します。 @@ -247,6 +252,11 @@ グローバル 'using' ディレクティブ + + path + path + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible 可能な場合、private フィールドを読み取り専用にする @@ -517,6 +527,11 @@ fixed ステートメント {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + 安全でないブロック + + using declaration using 宣言 diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf index 596c2ee5cba1..4b7d65f6b7f5 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ko.xlf @@ -32,6 +32,11 @@ NuGet 패키지 참조를 추가합니다. + + Adds a file reference. + 파일 참조를 추가합니다. + + Adds a project reference. 프로젝트 참조를 추가합니다. @@ -247,6 +252,11 @@ 전역 'using' 지시문 + + path + 경로 + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible 가능한 경우 프라이빗 필드를 읽기 전용으로 만들기 @@ -517,6 +527,11 @@ fixed 문 {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + 안전하지 않은 블록 + + using declaration using 선언 diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf index 14d9da3a42d3..ee79f5ca88c2 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pl.xlf @@ -32,6 +32,11 @@ Dodaje odwołanie do pakietu NuGet. + + Adds a file reference. + Dodaje odwołanie do pliku. + + Adds a project reference. Dodaje odwołanie do projektu. @@ -247,6 +252,11 @@ Globalne dyrektywy „using” + + path + ścieżka + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible Ustawiaj pola prywatne jako tylko do odczytu, gdy to możliwe @@ -517,6 +527,11 @@ instrukcja fixed {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + niebezpieczny blok + + using declaration deklaracja using diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf index c4ebaedae2f9..03f8287b1e36 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.pt-BR.xlf @@ -32,6 +32,11 @@ Adiciona uma referência de pacote NuGet. + + Adds a file reference. + Adiciona uma referência de arquivo. + + Adds a project reference. Adiciona uma referência de projeto. @@ -247,6 +252,11 @@ Diretivas 'using' global + + path + caminho + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible Tornar os campos privados somente leitura quando possível @@ -517,6 +527,11 @@ instrução fixed {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + bloco não seguro + + using declaration declaração using diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf index 8bc9f4536b4c..5a3eac2e22f4 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.ru.xlf @@ -32,6 +32,11 @@ Добавляет ссылку на пакет NuGet. + + Adds a file reference. + Добавляет ссылку на файл. + + Adds a project reference. Добавляет ссылку на проект. @@ -247,6 +252,11 @@ Глобальные директивы using + + path + путь + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible При возможности делать частные поля доступными только для чтения @@ -517,6 +527,11 @@ оператор fixed {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + небезопасный блок + + using declaration объявление using diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf index 330d5bf19605..2d3c33f65a2b 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.tr.xlf @@ -32,6 +32,11 @@ NNuGet paket başvurusu ekler. + + Adds a file reference. + Dosya başvurusu ekler. + + Adds a project reference. Proje başvurusu ekler. @@ -247,6 +252,11 @@ Genel 'using' yönergeleri + + path + yol + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible Özel alanları mümkün olduğunda salt okunur yap @@ -517,6 +527,11 @@ fixed deyimi {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + güvenli olmayan blok + + using declaration using bildirimi diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf index 80b26002d323..4561fb3db349 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hans.xlf @@ -32,6 +32,11 @@ 添加 NuGet 包引用。 + + Adds a file reference. + 添加文件引用。 + + Adds a project reference. 添加项目引用。 @@ -247,6 +252,11 @@ 全局 'using' 指令 + + path + 路径 + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible 尽可能将私有字段设置为“只读” @@ -517,6 +527,11 @@ fixed 语句 {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + 不安全代码块 + + using declaration using 声明 diff --git a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf index 2b6c70bdd79b..9c3383f62dec 100644 --- a/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf +++ b/src/Features/CSharp/Portable/xlf/CSharpFeaturesResources.zh-Hant.xlf @@ -32,6 +32,11 @@ 新增 NuGet 套件參考。 + + Adds a file reference. + 新增檔案參考。 + + Adds a project reference. 新增專案參考。 @@ -247,6 +252,11 @@ 全域 using 指示詞 + + path + 路徑 + 'path' is a placeholder for a file path or glob in a directive like '#:include path'. + Make private fields readonly when possible 若可能的話,請將私用欄位設定為唯讀 @@ -517,6 +527,11 @@ fixed 陳述式 {Locked="fixed"} "fixed" is a C# keyword and should not be localized. + + unsafe block + 不安全的區塊 + + using declaration using 宣告 diff --git a/src/Features/CSharpTest/ConvertCast/ConvertDirectCastToTryCastTests.cs b/src/Features/CSharpTest/ConvertCast/ConvertDirectCastToTryCastTests.cs index ced9ca4c14d2..a240164b1e03 100644 --- a/src/Features/CSharpTest/ConvertCast/ConvertDirectCastToTryCastTests.cs +++ b/src/Features/CSharpTest/ConvertCast/ConvertDirectCastToTryCastTests.cs @@ -297,36 +297,20 @@ public static void Main() }.RunAsync(); [Theory] - [InlineData("/* Leading */ (obj$$ect)1", - "/* Leading */ 1 as object")] - [InlineData("(obj$$ect)1 /* Trailing */", - "1 as object /* Trailing */")] - [InlineData("(obj$$ect)1; // Trailing", - "1 as object; // Trailing")] - [InlineData("(/* Middle1 */ obj$$ect)1", - """ - 1 as - /* Middle1 */ object - """)] - [InlineData("(obj$$ect /* Middle2 */ )1", - "1 as object /* Middle2 */ ")] - [InlineData("(obj$$ect) /* Middle3 */ 1", - "/* Middle3 */ 1 as object")] + [InlineData("/* Leading */ (obj$$ect)1", "/* Leading */ 1 as object")] + [InlineData("(obj$$ect)1 /* Trailing */", "1 as object /* Trailing */")] + [InlineData("(obj$$ect)1; // Trailing", "1 as object; // Trailing")] + [InlineData("(/* Middle1 */ obj$$ect)1", "1 as /* Middle1 */ object")] + [InlineData("(obj$$ect /* Middle2 */ )1", "1 as object /* Middle2 */ ")] + [InlineData("(obj$$ect) /* Middle3 */ 1", " /* Middle3 */ 1 as object")] [InlineData("/* Leading */ (/* Middle1 */ obj$$ect /* Middle2 */ ) /* Middle3 */ 1 /* Trailing */", - """ - /* Leading */ /* Middle3 */ 1 as - /* Middle1 */ object /* Middle2 */ /* Trailing */ - """)] + "/* Leading */ /* Middle3 */ 1 as /* Middle1 */ object /* Middle2 */ /* Trailing */")] [InlineData(""" ($$ object ) 1 - """, """ - - 1 as - object - """)] + """, "1 as object")] public Task ConvertFromExplicitToAs_Trivia(string cast, string asExpression) => new VerifyCS.Test { @@ -437,4 +421,35 @@ public static void Main() CodeActionValidationMode = CodeActionValidationMode.Full, }.RunAsync(); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/82576")] + public Task TestTrailingTrivia() + => new VerifyCS.Test + { + TestCode = """ + using System.Linq; + + class Program + { + void M(object o) + { + _ = from v in ([||]object[])o + select v; + } + } + """, + FixedCode = """ + using System.Linq; + + class Program + { + void M(object o) + { + _ = from v in o as object[] + select v; + } + } + """, + CodeActionValidationMode = CodeActionValidationMode.Full, + }.RunAsync(); } diff --git a/src/Features/CSharpTest/Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj b/src/Features/CSharpTest/Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj index f2472db46110..8a814ab3d6a3 100644 --- a/src/Features/CSharpTest/Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj +++ b/src/Features/CSharpTest/Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj @@ -4,7 +4,7 @@ Library - $(NetVSShared);net472 + $(NetRoslynWindowsTests);net472 Microsoft.CodeAnalysis.CSharp.UnitTests true diff --git a/src/Features/CSharpTest/Snippets/CSharpUnsafeSnippetProviderTests.cs b/src/Features/CSharpTest/Snippets/CSharpUnsafeSnippetProviderTests.cs new file mode 100644 index 000000000000..b46ea0f35ae4 --- /dev/null +++ b/src/Features/CSharpTest/Snippets/CSharpUnsafeSnippetProviderTests.cs @@ -0,0 +1,184 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Snippets; + +[Trait(Traits.Feature, Traits.Features.Snippets)] +public sealed class CSharpUnsafeSnippetProviderTests : AbstractCSharpSnippetProviderTests +{ + protected override string SnippetIdentifier => "unsafe"; + + [Fact] + public Task InsertUnsafeSnippetInMethodTest() + => VerifySnippetAsync(""" + class Program + { + public void Method() + { + $$ + } + } + """, """ + class Program + { + public void Method() + { + unsafe + { + $$ + } + } + } + """); + + [Fact] + public Task InsertUnsafeSnippetInGlobalContextTest() + => VerifySnippetAsync(""" + $$ + """, """ + unsafe + { + $$ + } + """); + + [Fact] + public Task NoUnsafeSnippetInBlockNamespaceTest() + => VerifySnippetIsAbsentAsync(""" + namespace Namespace + { + $$ + } + """); + + [Fact] + public Task NoUnsafeSnippetInFileScopedNamespaceTest() + => VerifySnippetIsAbsentAsync(""" + namespace Namespace; + $$ + """); + + [Fact] + public Task InsertUnsafeSnippetInConstructorTest() + => VerifySnippetAsync(""" + class Program + { + public Program() + { + $$ + } + } + """, """ + class Program + { + public Program() + { + unsafe + { + $$ + } + } + } + """); + + [Fact] + public Task NoUnsafeSnippetInTypeBodyTest() + => VerifySnippetIsAbsentAsync(""" + class Program + { + $$ + } + """); + + [Fact] + public Task InsertUnsafeSnippetInLocalFunctionTest() + => VerifySnippetAsync(""" + class Program + { + public void Method() + { + void LocalFunction() + { + $$ + } + } + } + """, """ + class Program + { + public void Method() + { + void LocalFunction() + { + unsafe + { + $$ + } + } + } + } + """); + + [Fact] + public Task InsertUnsafeSnippetInAnonymousFunctionTest() + => VerifySnippetAsync(""" + class Program + { + public void Method() + { + var action = delegate() + { + $$ + }; + } + } + """, """ + class Program + { + public void Method() + { + var action = delegate() + { + unsafe + { + $$ + } + }; + } + } + """); + + [Fact] + public Task InsertUnsafeSnippetInParenthesizedLambdaExpressionTest() + => VerifySnippetAsync(""" + class Program + { + public void Method() + { + var action = () => + { + $$ + }; + } + } + """, """ + class Program + { + public void Method() + { + var action = () => + { + unsafe + { + $$ + } + }; + } + } + """); +} diff --git a/src/Features/CSharpTest/SplitOrMergeIfStatements/SplitIntoConsecutiveIfStatementsTests.cs b/src/Features/CSharpTest/SplitOrMergeIfStatements/SplitIntoConsecutiveIfStatementsTests.cs index 2f7b881cff25..9fcff828a96c 100644 --- a/src/Features/CSharpTest/SplitOrMergeIfStatements/SplitIntoConsecutiveIfStatementsTests.cs +++ b/src/Features/CSharpTest/SplitOrMergeIfStatements/SplitIntoConsecutiveIfStatementsTests.cs @@ -1403,4 +1403,21 @@ void M(bool a, bool b) } } """); + + [Fact] + public Task SplitIntoConsecutiveIfStatements_TopLevelStatement() + => TestInRegularAndScriptAsync( + """ + if (a [||]|| b) + { + } + """, + """ + if (a) + { + } + else if (b) + { + } + """); } diff --git a/src/Features/CSharpTest/SplitOrMergeIfStatements/SplitIntoNestedIfStatementsTests.cs b/src/Features/CSharpTest/SplitOrMergeIfStatements/SplitIntoNestedIfStatementsTests.cs index c450e8c4a936..b73c0c61f092 100644 --- a/src/Features/CSharpTest/SplitOrMergeIfStatements/SplitIntoNestedIfStatementsTests.cs +++ b/src/Features/CSharpTest/SplitOrMergeIfStatements/SplitIntoNestedIfStatementsTests.cs @@ -824,4 +824,21 @@ void M(bool a, bool b) } } """); + + [Fact] + public Task SplitIntoNestedIfStatements_TopLevelStatement() + => TestInRegularAndScriptAsync( + """ + if (a [||]&& b) + { + } + """, + """ + if (a) + { + if (b) + { + } + } + """); } diff --git a/src/Features/Core/Portable/CallHierarchy/AbstractCallHierarchyService.cs b/src/Features/Core/Portable/CallHierarchy/AbstractCallHierarchyService.cs new file mode 100644 index 000000000000..28b59c214306 --- /dev/null +++ b/src/Features/Core/Portable/CallHierarchy/AbstractCallHierarchyService.cs @@ -0,0 +1,374 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Operations; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.CallHierarchy; + +internal abstract class AbstractCallHierarchyService : ICallHierarchyService +{ + public async Task CreateItemAsync(ISymbol symbol, Project project, CancellationToken cancellationToken) + { + if (!SupportsCallHierarchy(symbol)) + return null; + + symbol = GetTargetSymbol(symbol); + + if (!CallHierarchyItemId.TryCreate(symbol, project, cancellationToken, out var itemId)) + return null; + + var searchDescriptors = await CreateSearchDescriptorsAsync(symbol, project, cancellationToken).ConfigureAwait(false); + return new CallHierarchyItemDescriptor( + itemId, + symbol.ToDisplayString(CallHierarchyDisplayFormats.MemberNameFormat), + symbol.ContainingType?.ToDisplayString(CallHierarchyDisplayFormats.ContainingTypeFormat) ?? string.Empty, + symbol.ContainingNamespace?.ToDisplayString(CallHierarchyDisplayFormats.ContainingNamespaceFormat) ?? string.Empty, + symbol.GetGlyph(), + searchDescriptors); + } + + public async Task> SearchIncomingCallsAsync( + Solution solution, + CallHierarchySearchDescriptor searchDescriptor, + IImmutableSet? documents, + CancellationToken cancellationToken) + { + var resolved = await searchDescriptor.ItemId.TryResolveAsync(solution, cancellationToken).ConfigureAwait(false); + if (resolved == null) + return []; + + var (symbol, project) = resolved.Value; + return searchDescriptor.Relationship switch + { + CallHierarchyRelationshipKind.Callers or + CallHierarchyRelationshipKind.BaseMember or + CallHierarchyRelationshipKind.InterfaceImplementations or + CallHierarchyRelationshipKind.FieldReferences => await SearchCallersAsync(symbol, project, documents, cancellationToken).ConfigureAwait(false), + CallHierarchyRelationshipKind.CallsToOverrides => await SearchCallsToOverridesAsync(symbol, project, documents, cancellationToken).ConfigureAwait(false), + CallHierarchyRelationshipKind.Implementations => await SearchImplementationsAsync(symbol, project, documents, cancellationToken).ConfigureAwait(false), + CallHierarchyRelationshipKind.Overrides => await SearchOverridesAsync(symbol, project, documents, cancellationToken).ConfigureAwait(false), + _ => [], + }; + } + + public async Task> SearchOutgoingCallsAsync( + Solution solution, + CallHierarchyItemId itemId, + IImmutableSet? documents, + CancellationToken cancellationToken) + { + var resolved = await itemId.TryResolveAsync(solution, cancellationToken).ConfigureAwait(false); + if (resolved == null) + return []; + + var (symbol, project) = resolved.Value; + return await SearchOutgoingCallsAsync(symbol, project, documents, cancellationToken).ConfigureAwait(false); + } + + private static bool SupportsCallHierarchy(ISymbol symbol) + => symbol.Kind is SymbolKind.Method or SymbolKind.Property or SymbolKind.Event or SymbolKind.Field; + + private static ISymbol GetTargetSymbol(ISymbol symbol) + { + if (symbol is IMethodSymbol methodSymbol) + { + methodSymbol = methodSymbol.ReducedFrom ?? methodSymbol; + methodSymbol = methodSymbol.ConstructedFrom ?? methodSymbol; + return methodSymbol; + } + + return symbol; + } + + private static async Task> CreateSearchDescriptorsAsync( + ISymbol symbol, + Project project, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var descriptors); + + if (symbol.Kind is SymbolKind.Property or SymbolKind.Event or SymbolKind.Method) + { + descriptors.Add(new CallHierarchySearchDescriptor( + CallHierarchyRelationshipKind.Callers, + CallHierarchyItemId.Create(symbol, project, cancellationToken))); + + if (symbol.IsVirtual || symbol.IsAbstract) + { + descriptors.Add(new CallHierarchySearchDescriptor( + CallHierarchyRelationshipKind.Overrides, + CallHierarchyItemId.Create(symbol, project, cancellationToken))); + } + + var overrides = await SymbolFinder.FindOverridesAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); + if (overrides.Any()) + { + descriptors.Add(new CallHierarchySearchDescriptor( + CallHierarchyRelationshipKind.CallsToOverrides, + CallHierarchyItemId.Create(symbol, project, cancellationToken))); + } + + if (symbol.GetOverriddenMember() is ISymbol overriddenMember && + CallHierarchyItemId.TryCreate(overriddenMember, project, cancellationToken, out var overriddenItemId)) + { + descriptors.Add(new CallHierarchySearchDescriptor( + CallHierarchyRelationshipKind.BaseMember, + overriddenItemId)); + } + + var implementedInterfaceMembers = await SymbolFinder.FindImplementedInterfaceMembersAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); + foreach (var implementedInterfaceMember in implementedInterfaceMembers) + { + if (!CallHierarchyItemId.TryCreate(implementedInterfaceMember, project, cancellationToken, out var interfaceItemId)) + continue; + + descriptors.Add(new CallHierarchySearchDescriptor( + CallHierarchyRelationshipKind.InterfaceImplementations, + interfaceItemId)); + } + + if (symbol.IsImplementableMember()) + { + descriptors.Add(new CallHierarchySearchDescriptor( + CallHierarchyRelationshipKind.Implementations, + CallHierarchyItemId.Create(symbol, project, cancellationToken))); + } + + return descriptors.ToImmutableAndClear(); + } + + if (symbol.Kind == SymbolKind.Field) + { + descriptors.Add(new CallHierarchySearchDescriptor( + CallHierarchyRelationshipKind.FieldReferences, + CallHierarchyItemId.Create(symbol, project, cancellationToken))); + } + + return descriptors.ToImmutableAndClear(); + } + + private async Task> SearchCallersAsync( + ISymbol symbol, + Project project, + IImmutableSet? documents, + CancellationToken cancellationToken) + { + var callers = await SymbolFinder.FindCallersAsync(symbol, project.Solution, documents, cancellationToken).ConfigureAwait(false); + return await CreateCallerResultsAsync(callers.Where(static c => c.IsDirect), project, cancellationToken).ConfigureAwait(false); + } + + private async Task> SearchCallsToOverridesAsync( + ISymbol symbol, + Project project, + IImmutableSet? documents, + CancellationToken cancellationToken) + { + var overrides = await SymbolFinder.FindOverridesAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); + using var _ = ArrayBuilder.GetInstance(out var callers); + + foreach (var @override in overrides) + { + var calls = await SymbolFinder.FindCallersAsync(@override, project.Solution, documents, cancellationToken).ConfigureAwait(false); + callers.AddRange(calls.Where(static c => c.IsDirect)); + cancellationToken.ThrowIfCancellationRequested(); + } + + return await CreateCallerResultsAsync(callers, project, cancellationToken).ConfigureAwait(false); + } + + private async Task> SearchImplementationsAsync( + ISymbol symbol, + Project project, + IImmutableSet? documents, + CancellationToken cancellationToken) + { + var implementations = await SymbolFinder.FindImplementationsAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); + return await CreateSourceDeclarationResultsAsync(implementations, project, documents, cancellationToken).ConfigureAwait(false); + } + + private async Task> SearchOverridesAsync( + ISymbol symbol, + Project project, + IImmutableSet? documents, + CancellationToken cancellationToken) + { + var overrides = await SymbolFinder.FindOverridesAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); + return await CreateSourceDeclarationResultsAsync(overrides, project, documents, cancellationToken).ConfigureAwait(false); + } + + private async Task> SearchOutgoingCallsAsync( + ISymbol symbol, + Project project, + IImmutableSet? documents, + CancellationToken cancellationToken) + { + using var _ = PooledDictionary Locations)>.GetInstance(out var groupedResults); + + foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) + { + var document = project.Solution.GetDocument(syntaxReference.SyntaxTree); + if (document == null || (documents != null && !documents.Contains(document))) + continue; + + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var operationRoots = await GetOperationRootsAsync(syntaxReference, semanticModel, cancellationToken).ConfigureAwait(false); + + foreach (var operationRoot in operationRoots) + { + foreach (var operation in operationRoot.Descendants()) + { + var referencedSymbol = GetReferencedSymbol(operation); + if (referencedSymbol == null || !SupportsCallHierarchy(referencedSymbol)) + continue; + + var referencedProject = project.Solution.GetProject(referencedSymbol.ContainingAssembly, cancellationToken); + if (referencedProject == null) + continue; + + var item = await CreateItemAsync(referencedSymbol, referencedProject, cancellationToken).ConfigureAwait(false); + if (item == null) + continue; + + if (!groupedResults.TryGetValue(item.ItemId, out var entry)) + { + entry = (item, ArrayBuilder.GetInstance()); + groupedResults.Add(item.ItemId, entry); + } + + entry.Locations.Add(operation.Syntax.GetLocation()); + cancellationToken.ThrowIfCancellationRequested(); + } + } + } + + using var __ = ArrayBuilder.GetInstance(out var results); + foreach (var (_, value) in groupedResults) + { + results.Add(new CallHierarchySearchResult(value.Item, value.Locations.ToImmutableAndClear())); + } + + return results.ToImmutableAndClear(); + } + + private async Task> CreateCallerResultsAsync( + IEnumerable callers, + Project project, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var results); + using var __ = ArrayBuilder.GetInstance(out var initializerLocations); + + foreach (var caller in callers) + { + if (caller.CallingSymbol.Kind == SymbolKind.Field) + { + initializerLocations.AddRange(caller.Locations); + continue; + } + + var callingProject = project.Solution.GetProject(caller.CallingSymbol.ContainingAssembly, cancellationToken); + if (callingProject == null) + continue; + + var item = await CreateItemAsync(caller.CallingSymbol, callingProject, cancellationToken).ConfigureAwait(false); + if (item != null) + results.Add(new CallHierarchySearchResult(item, [.. caller.Locations])); + + cancellationToken.ThrowIfCancellationRequested(); + } + + if (initializerLocations.Count > 0) + results.Add(new CallHierarchySearchResult(Item: null, initializerLocations.ToImmutable())); + + return results.ToImmutableAndClear(); + } + + private async Task> CreateSourceDeclarationResultsAsync( + IEnumerable symbols, + Project project, + IImmutableSet? documents, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var results); + + foreach (var symbol in symbols) + { + var sourceLocations = symbol.DeclaringSyntaxReferences.Select(static d => d.SyntaxTree) + .Select(project.Solution.GetDocument) + .Where(static d => d != null) + .Select(static d => d!); + var bestLocation = sourceLocations.FirstOrDefault(d => documents == null || documents.Contains(d)); + if (bestLocation == null) + continue; + + var item = await CreateItemAsync(symbol, bestLocation.Project, cancellationToken).ConfigureAwait(false); + if (item != null) + results.Add(new CallHierarchySearchResult(item, [])); + + cancellationToken.ThrowIfCancellationRequested(); + } + + return results.ToImmutableAndClear(); + } + + private static IOperation? GetReferencedSymbolOperationRoot(IOperation operation) + { + while (operation.Parent != null) + operation = operation.Parent; + + return operation; + } + + private async Task> GetOperationRootsAsync( + SyntaxReference syntaxReference, + SemanticModel semanticModel, + CancellationToken cancellationToken) + { + var syntax = await GetOperationRootSyntaxAsync(syntaxReference, cancellationToken).ConfigureAwait(false); + + using var _ = ArrayBuilder.GetInstance(out var operations); + using var __ = PooledHashSet.GetInstance(out var seenRootSyntaxes); + + AddOperation(semanticModel.GetOperation(syntax, cancellationToken)); + + foreach (var descendant in syntax.DescendantNodesAndSelf()) + { + AddOperation(semanticModel.GetOperation(descendant, cancellationToken)); + } + + return operations.ToImmutableAndClear(); + + void AddOperation(IOperation? operation) + { + if (operation == null) + return; + + var root = GetReferencedSymbolOperationRoot(operation); + if (root != null && seenRootSyntaxes.Add(root.Syntax)) + operations.Add(root); + } + } + + protected virtual Task GetOperationRootSyntaxAsync(SyntaxReference syntaxReference, CancellationToken cancellationToken) + => syntaxReference.GetSyntaxAsync(cancellationToken); + + private static ISymbol? GetReferencedSymbol(IOperation operation) + => operation switch + { + IInvocationOperation invocation => invocation.TargetMethod, + IObjectCreationOperation objectCreation => objectCreation.Constructor, + IPropertyReferenceOperation propertyReference => propertyReference.Property, + IEventReferenceOperation eventReference => eventReference.Event, + IFieldReferenceOperation fieldReference => fieldReference.Field, + _ => null, + }; +} diff --git a/src/Features/Core/Portable/CallHierarchy/CallHierarchyDisplayFormats.cs b/src/Features/Core/Portable/CallHierarchy/CallHierarchyDisplayFormats.cs new file mode 100644 index 000000000000..75ce58699ca3 --- /dev/null +++ b/src/Features/Core/Portable/CallHierarchy/CallHierarchyDisplayFormats.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.CallHierarchy; + +internal static class CallHierarchyDisplayFormats +{ + public static readonly SymbolDisplayFormat MemberNameFormat = + new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, + propertyStyle: SymbolDisplayPropertyStyle.NameOnly, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + memberOptions: SymbolDisplayMemberOptions.IncludeParameters | SymbolDisplayMemberOptions.IncludeExplicitInterface, + parameterOptions: + SymbolDisplayParameterOptions.IncludeParamsRefOut | + SymbolDisplayParameterOptions.IncludeExtensionThis | + SymbolDisplayParameterOptions.IncludeType, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + public static readonly SymbolDisplayFormat ContainingTypeFormat = + new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, + genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, + miscellaneousOptions: + SymbolDisplayMiscellaneousOptions.UseSpecialTypes); + + public static readonly SymbolDisplayFormat ContainingNamespaceFormat = + new( + globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, + typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); +} diff --git a/src/Features/Core/Portable/CallHierarchy/CallHierarchyItemDescriptor.cs b/src/Features/Core/Portable/CallHierarchy/CallHierarchyItemDescriptor.cs new file mode 100644 index 000000000000..43a09578b556 --- /dev/null +++ b/src/Features/Core/Portable/CallHierarchy/CallHierarchyItemDescriptor.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.CallHierarchy; + +internal sealed record CallHierarchyItemDescriptor( + CallHierarchyItemId ItemId, + string MemberName, + string ContainingTypeName, + string ContainingNamespaceName, + Glyph Glyph, + ImmutableArray SupportedSearchDescriptors); diff --git a/src/Features/Core/Portable/CallHierarchy/CallHierarchyItemId.cs b/src/Features/Core/Portable/CallHierarchy/CallHierarchyItemId.cs new file mode 100644 index 000000000000..1f089247fac2 --- /dev/null +++ b/src/Features/Core/Portable/CallHierarchy/CallHierarchyItemId.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.CallHierarchy; + +internal sealed record CallHierarchyItemId(string SymbolKeyData, ProjectId ProjectId) +{ + public static CallHierarchyItemId Create(ISymbol symbol, Project project, CancellationToken cancellationToken) + => new(SymbolKey.CreateString(symbol, cancellationToken), project.Id); + + public static bool TryCreate( + ISymbol symbol, + Project project, + CancellationToken cancellationToken, + [NotNullWhen(true)] + out CallHierarchyItemId? itemId) + { + if (!SymbolKey.CanCreate(symbol, cancellationToken)) + { + itemId = null; + return false; + } + + itemId = Create(symbol, project, cancellationToken); + return true; + } + + public async Task<(ISymbol Symbol, Project Project)?> TryResolveAsync(Solution solution, CancellationToken cancellationToken) + { + var project = solution.GetProject(ProjectId); + if (project == null) + return null; + + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation == null) + return null; + + var symbol = SymbolKey.ResolveString(SymbolKeyData, compilation, cancellationToken: cancellationToken).GetAnySymbol(); + return symbol == null ? null : (symbol, project); + } +} diff --git a/src/Features/Core/Portable/CallHierarchy/CallHierarchyItemSearchResult.cs b/src/Features/Core/Portable/CallHierarchy/CallHierarchyItemSearchResult.cs new file mode 100644 index 000000000000..1fb84aac6e53 --- /dev/null +++ b/src/Features/Core/Portable/CallHierarchy/CallHierarchyItemSearchResult.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.CallHierarchy; + +internal sealed record CallHierarchySearchResult( + CallHierarchyItemDescriptor? Item, + ImmutableArray ReferenceLocations); diff --git a/src/Features/Core/Portable/CallHierarchy/CallHierarchyRelationshipKind.cs b/src/Features/Core/Portable/CallHierarchy/CallHierarchyRelationshipKind.cs new file mode 100644 index 000000000000..d414a82a45d2 --- /dev/null +++ b/src/Features/Core/Portable/CallHierarchy/CallHierarchyRelationshipKind.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.CallHierarchy; + +internal enum CallHierarchyRelationshipKind +{ + Callers, + CallsToOverrides, + BaseMember, + InterfaceImplementations, + Implementations, + Overrides, + FieldReferences, +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/NamedPipeInformation.cs b/src/Features/Core/Portable/CallHierarchy/CallHierarchySearchDescriptor.cs similarity index 51% rename from src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/NamedPipeInformation.cs rename to src/Features/Core/Portable/CallHierarchy/CallHierarchySearchDescriptor.cs index 5d0e357c16c1..d07000afb36b 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/NamedPipeInformation.cs +++ b/src/Features/Core/Portable/CallHierarchy/CallHierarchySearchDescriptor.cs @@ -2,9 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Text.Json.Serialization; +namespace Microsoft.CodeAnalysis.CallHierarchy; -namespace Microsoft.CodeAnalysis.LanguageServer; - -internal sealed record NamedPipeInformation( - [property: JsonPropertyName("pipeName")] string PipeName); +internal sealed record CallHierarchySearchDescriptor( + CallHierarchyRelationshipKind Relationship, + CallHierarchyItemId ItemId); diff --git a/src/Features/Core/Portable/CallHierarchy/ICallHierarchyService.cs b/src/Features/Core/Portable/CallHierarchy/ICallHierarchyService.cs new file mode 100644 index 000000000000..8f091d4a612f --- /dev/null +++ b/src/Features/Core/Portable/CallHierarchy/ICallHierarchyService.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.CallHierarchy; + +internal interface ICallHierarchyService : ILanguageService +{ + Task CreateItemAsync(ISymbol symbol, Project project, CancellationToken cancellationToken); + + Task> SearchIncomingCallsAsync( + Solution solution, + CallHierarchySearchDescriptor searchDescriptor, + IImmutableSet? documents, + CancellationToken cancellationToken); + + Task> SearchOutgoingCallsAsync( + Solution solution, + CallHierarchyItemId itemId, + IImmutableSet? documents, + CancellationToken cancellationToken); +} diff --git a/src/Features/Core/Portable/CodeFixes/Service/CodeFixService.cs b/src/Features/Core/Portable/CodeFixes/Service/CodeFixService.cs index 6520cd7fb624..231e59c8e2ae 100644 --- a/src/Features/Core/Portable/CodeFixes/Service/CodeFixService.cs +++ b/src/Features/Core/Portable/CodeFixes/Service/CodeFixService.cs @@ -83,11 +83,13 @@ private DiagnosticIdFilter GetShouldIncludeDiagnosticPredicate( if (!(priority is CodeActionRequestPriority.Default or CodeActionRequestPriority.Low)) return DiagnosticIdFilter.All; - TryGetWorkspaceFixersMap(document, out var workspaceFixersMap); - workspaceFixersMap ??= ImmutableDictionary>.Empty; + using var _ = PooledDictionary>.GetInstance(out var builder); + TryPopulateWorkspaceFixersMap(document, builder); var projectFixersMap = GetProjectFixers(document); - return DiagnosticIdFilter.Include([.. workspaceFixersMap.Keys, .. projectFixersMap.Keys]); + // Perf: Avoid using a collection expression as it allocates an unnecessary list showing up in profiles + var set = ImmutableHashSet.CreateRange(builder.Keys.Concat(projectFixersMap.Keys)); + return DiagnosticIdFilter.Include(set); } public async Task GetMostSevereFixAsync( @@ -354,7 +356,7 @@ public async Task ApplyCodeFixesForSpecificDiagnosticIdAsync( return solution.GetDocument(document.Id) ?? throw new NotSupportedException(FeaturesResources.Removal_of_document_not_supported); } - private bool TryGetWorkspaceFixersMap(TextDocument document, [NotNullWhen(true)] out ImmutableDictionary>? fixerMap) + private bool TryPopulateWorkspaceFixersMap(TextDocument document, Dictionary> fixerMap) { if (_lazyWorkspaceFixersMap == null) { @@ -364,19 +366,16 @@ private bool TryGetWorkspaceFixersMap(TextDocument document, [NotNullWhen(true)] if (!_lazyWorkspaceFixersMap.TryGetValue(document.Project.Language, out var lazyFixerMap)) { - fixerMap = ImmutableDictionary>.Empty; return false; } - using var _ = PooledDictionary>.GetInstance(out var builder); foreach (var (id, fixers) in lazyFixerMap.Value) { var filteredFixers = ProjectCodeFixProvider.FilterExtensions(document, fixers, GetExtensionInfo); if (!filteredFixers.IsEmpty) - builder.Add(id, filteredFixers); + fixerMap.Add(id, filteredFixers); } - fixerMap = builder.ToImmutableDictionary(); return fixerMap.Count > 0; } @@ -442,7 +441,8 @@ private async IAsyncEnumerable StreamFixesAsync( { cancellationToken.ThrowIfCancellationRequested(); - var hasAnySharedFixer = TryGetWorkspaceFixersMap(document, out var fixerMap); + using var _1 = PooledDictionary>.GetInstance(out var fixerMap); + var hasAnySharedFixer = TryPopulateWorkspaceFixersMap(document, fixerMap); var projectFixersMap = GetProjectFixers(document); var hasAnyProjectFixer = !projectFixersMap.IsEmpty; @@ -454,8 +454,8 @@ private async IAsyncEnumerable StreamFixesAsync( var isInteractive = document.Project.Solution.WorkspaceKind == WorkspaceKind.Interactive; // gather CodeFixProviders for all distinct diagnostics found for current span - using var _1 = PooledDictionary diagnostics)>>.GetInstance(out var fixerToRangesAndDiagnostics); - using var _2 = PooledHashSet.GetInstance(out var currentFixers); + using var _2 = PooledDictionary diagnostics)>>.GetInstance(out var fixerToRangesAndDiagnostics); + using var _3 = PooledHashSet.GetInstance(out var currentFixers); foreach (var (range, diagnostics) in spanToDiagnostics) { @@ -472,7 +472,7 @@ private async IAsyncEnumerable StreamFixesAsync( AddAllFixers(projectFixers, range, diagnostics, currentFixers, fixerToRangesAndDiagnostics); } - if (hasAnySharedFixer && fixerMap!.TryGetValue(diagnosticId, out var workspaceFixers)) + if (hasAnySharedFixer && fixerMap.TryGetValue(diagnosticId, out var workspaceFixers)) { if (isInteractive) { diff --git a/src/Features/Core/Portable/Common/GlyphExtensions.cs b/src/Features/Core/Portable/Common/GlyphExtensions.cs index 6f54b3c7655a..2d5e6defc884 100644 --- a/src/Features/Core/Portable/Common/GlyphExtensions.cs +++ b/src/Features/Core/Portable/Common/GlyphExtensions.cs @@ -297,6 +297,7 @@ private static Glyph GetPublicGlyph(DeclaredSymbolInfoKind kind) DeclaredSymbolInfoKind.Property => Glyph.PropertyPublic, DeclaredSymbolInfoKind.Struct => Glyph.StructurePublic, DeclaredSymbolInfoKind.RecordStruct => Glyph.StructurePublic, + DeclaredSymbolInfoKind.Union => Glyph.StructurePublic, _ => Glyph.ClassPublic, }; } diff --git a/src/Features/Core/Portable/Completion/CommonCompletionItem.cs b/src/Features/Core/Portable/Completion/CommonCompletionItem.cs index 87462b6f19c5..5ddf3b2d2849 100644 --- a/src/Features/Core/Portable/Completion/CommonCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/CommonCompletionItem.cs @@ -15,6 +15,15 @@ internal static class CommonCompletionItem { public const string DescriptionProperty = nameof(DescriptionProperty); + /// + /// Mark CompletionItem with this property to indicate the intention of keeping otherwise similar items separated in the completion list. + /// By default, when two items with identical texts are added to the completion list, unless both are marked with this property, + /// only one of them will be shown (the one with this property will win, if exist). + /// For example, when there are two items, a property `MyClass.MyMember` and an exntension method `ExtensionClass.MyMember(this MyClass c)` + /// they have same display text `MyMember` but we want to show both of them in the completion list. + /// + public const string DoNotMergeProperty = nameof(DoNotMergeProperty); + public static CompletionItem Create( string displayText, string? displayTextSuffix, diff --git a/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs b/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs index bd55a43292ec..55d0d54c3df4 100644 --- a/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs +++ b/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.Extensions; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageService; @@ -287,8 +288,11 @@ private CompletionList MergeAndPruneCompletionLists( /// protected virtual bool ItemsMatch(CompletionItem item, CompletionItem existingItem) { + // If both items is marked as `DoNotMerge`, we will keep them separated in the list. + // If only one of them is marked as `DoNotMerge`, the one with `DoNotMerge` is considered "better" item and will be merged into. return item.Span == existingItem.Span - && item.SortText == existingItem.SortText && item.InlineDescription == existingItem.InlineDescription; + && item.SortText == existingItem.SortText && item.InlineDescription == existingItem.InlineDescription + && !(item.TryGetProperty(CommonCompletionItem.DoNotMergeProperty, out _) && existingItem.TryGetProperty(CommonCompletionItem.DoNotMergeProperty, out _)); } /// @@ -296,6 +300,15 @@ protected virtual bool ItemsMatch(CompletionItem item, CompletionItem existingIt /// protected virtual CompletionItem GetBetterItem(CompletionItem item, CompletionItem existingItem) { + if (item.TryGetProperty(CommonCompletionItem.DoNotMergeProperty, out _)) + { + return item; + } + else if (existingItem.TryGetProperty(CommonCompletionItem.DoNotMergeProperty, out _)) + { + return existingItem; + } + // the item later in the sort order (determined by provider order) wins? return item; } diff --git a/src/Features/Core/Portable/Completion/PatternMatchHelper.cs b/src/Features/Core/Portable/Completion/PatternMatchHelper.cs index 12b6ad2a4189..0ede4d35ffc9 100644 --- a/src/Features/Core/Portable/Completion/PatternMatchHelper.cs +++ b/src/Features/Core/Portable/Completion/PatternMatchHelper.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -86,8 +86,7 @@ private PatternMatcher GetPatternMatcher(CultureInfo culture, bool includeMatche if (!_patternMatcherMap.TryGetValue(key, out var patternMatcher)) { patternMatcher = PatternMatcher.CreatePatternMatcher( - Pattern, culture, includeMatchedSpans, - allowFuzzyMatching: false); + Pattern, culture, includeMatchedSpans); _patternMatcherMap.Add(key, patternMatcher); } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractRecommendationServiceBasedCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractRecommendationServiceBasedCompletionProvider.cs index 987a0aaf0e27..d824d7a8d499 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractRecommendationServiceBasedCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractRecommendationServiceBasedCompletionProvider.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Recommendations; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -301,4 +302,10 @@ protected sealed override async Task IsSemanticTriggerCharacterAsync(Docum return IsTriggerOnDot(token, characterPosition); } + + protected override ImmutableArray> DeduplicateSymbols( + MultiDictionary<(string displayText, string suffix, string insertionText), SymbolAndSelectionInfo>.ValueSet symbols) + { + return symbols.GroupBy(symbol => symbol.Symbol.Kind).Select(group => group.ToImmutableArray()).ToImmutableArray(); + } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs index 1a11cdd1da82..63478cbdd07d 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs @@ -177,42 +177,71 @@ private ImmutableArray CreateItems( foreach (var symbolGroup in symbolGroups) { - var includeItemInTargetTypedCompletion = false; - using var symbolListBuilder = TemporaryArray.Empty; - foreach (var symbol in symbolGroup.Value) - symbolListBuilder.Add(symbol); - - var symbolList = symbolListBuilder.ToImmutableAndClear(); - var arbitraryFirstContext = contextLookup(symbolList[0]); - - if (completionContext.CompletionOptions.TargetTypedCompletionFilter) + if (symbolGroup.Value.Count == 1) + { + // It's very rare to have multiple symbols with identical texts, + // so we optimize for single symbol case to minimize overhead. + CreateAndAddItem([symbolGroup.Value.Single()], false); + } + else { - includeItemInTargetTypedCompletion = TryFindFirstSymbolMatchesTargetTypes(contextLookup, symbolList, typeConvertibilityCache, out var index); - if (includeItemInTargetTypedCompletion && index > 0) + var symbolLists = DeduplicateSymbols(symbolGroup.Value); + var noMerge = symbolLists.Length > 1; + foreach (var symbolList in symbolLists) { - // This would ensure a symbol matches target types to be used for description if there's any, - // assuming the default implementation of GetDescriptionWorkerAsync is used. - var firstMatch = symbolList[index]; - symbolList = symbolList.RemoveAt(index); - symbolList = symbolList.Insert(0, firstMatch); + CreateAndAddItem(symbolList, noMerge); } } - var supportedPlatformData = ComputeSupportedPlatformData(completionContext, symbolList, invalidProjectMap, totalProjects); - var item = CreateItem( - completionContext, symbolGroup.Key.displayText, symbolGroup.Key.suffix, symbolGroup.Key.insertionText, symbolList, arbitraryFirstContext, supportedPlatformData); - - if (includeItemInTargetTypedCompletion) + void CreateAndAddItem(ImmutableArray symbolList, bool doNotMerge) { - item = item.AddTag(WellKnownTags.TargetTypeMatch); - } + var includeItemInTargetTypedCompletion = false; + var arbitraryFirstContext = contextLookup(symbolList[0]); + + if (completionContext.CompletionOptions.TargetTypedCompletionFilter) + { + includeItemInTargetTypedCompletion = TryFindFirstSymbolMatchesTargetTypes(contextLookup, symbolList, typeConvertibilityCache, out var index); + if (includeItemInTargetTypedCompletion && index > 0) + { + // This would ensure a symbol matches target types to be used for description if there's any, + // assuming the default implementation of GetDescriptionWorkerAsync is used. + var firstMatch = symbolList[index]; + symbolList = symbolList.RemoveAt(index); + symbolList = symbolList.Insert(0, firstMatch); + } + } + + var supportedPlatformData = ComputeSupportedPlatformData(completionContext, symbolList, invalidProjectMap, totalProjects); + var item = CreateItem( + completionContext, symbolGroup.Key.displayText, symbolGroup.Key.suffix, symbolGroup.Key.insertionText, symbolList, arbitraryFirstContext, supportedPlatformData); + + if (includeItemInTargetTypedCompletion) + { + item = item.AddTag(WellKnownTags.TargetTypeMatch); + } + + if (doNotMerge) + { + item = item.AddProperty(CommonCompletionItem.DoNotMergeProperty, string.Empty); + } - itemListBuilder.Add(item); + itemListBuilder.Add(item); + } } return itemListBuilder.ToImmutableAndClear(); } + protected virtual ImmutableArray> DeduplicateSymbols( + MultiDictionary<(string displayText, string suffix, string insertionText), SymbolAndSelectionInfo>.ValueSet symbols) + { + using var symbolListBuilder = TemporaryArray.Empty; + foreach (var symbol in symbols) + symbolListBuilder.Add(symbol); + + return [symbolListBuilder.ToImmutableAndClear()]; + } + /// /// Alternative comparer to SymbolAndSelectionInfo's default which considers both the full symbol and preselect. /// diff --git a/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs index aab958e23452..f5f617723e6f 100644 --- a/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs @@ -23,6 +23,8 @@ internal static class SymbolCompletionItem private static readonly Action, ArrayBuilder>> s_addSymbolEncoding = AddSymbolEncoding; private static readonly Action, ArrayBuilder>> s_addSymbolInfo = AddSymbolInfo; + private static ContextPositionCache s_lastContextPositionInfo = new(0, "0"); + private const char ProjectSeparatorChar = ';'; private static CompletionItem CreateWorker( @@ -51,7 +53,7 @@ private static CompletionItem CreateWorker( if (insertionText != null) builder.Add(KeyValuePair.Create(InsertionTextProperty, insertionText)); - builder.Add(KeyValuePair.Create("ContextPosition", contextPosition.ToString())); + AddContextPosition(builder, contextPosition); AddSupportedPlatforms(builder, supportedPlatforms); symbolEncoder(symbols, builder); @@ -225,6 +227,26 @@ private static Document FindAppropriateDocumentForDescriptionContext(Document do return document; } + private static void AddContextPosition(ArrayBuilder> properties, int contextPosition) + { + // Cache the last context position we added to avoid doing extra allocations of converting int to string. + // Nearly all completion items for a session have the same context position. + var contextPositionData = s_lastContextPositionInfo; + + string contextPositionString; + if (contextPositionData.Position == contextPosition) + { + contextPositionString = contextPositionData.PositionString; + } + else + { + contextPositionString = contextPosition.ToString(); + s_lastContextPositionInfo = new ContextPositionCache(contextPosition, contextPositionString); + } + + properties.Add(KeyValuePair.Create("ContextPosition", contextPositionString)); + } + private static void AddSupportedPlatforms(ArrayBuilder> properties, SupportedPlatformData? supportedPlatforms) { if (supportedPlatforms != null) @@ -406,4 +428,10 @@ public static async Task GetDescriptionAsync( return CompletionDescription.Empty; } } + + private sealed class ContextPositionCache(int position, string positionString) + { + public readonly int Position = position; + public readonly string PositionString = positionString; + } } diff --git a/src/Features/Core/Portable/Contracts/Client/ISolutionSnapshotProvider.cs b/src/Features/Core/Portable/Contracts/Client/ISolutionSnapshotProviderService.cs similarity index 92% rename from src/Features/Core/Portable/Contracts/Client/ISolutionSnapshotProvider.cs rename to src/Features/Core/Portable/Contracts/Client/ISolutionSnapshotProviderService.cs index cdcdb86d3c14..6c428049a13d 100644 --- a/src/Features/Core/Portable/Contracts/Client/ISolutionSnapshotProvider.cs +++ b/src/Features/Core/Portable/Contracts/Client/ISolutionSnapshotProviderService.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.Contracts.Client; internal readonly record struct SolutionSnapshotId([property: DataMember] int Id); // brokered service implemented by the client -internal interface ISolutionSnapshotProvider +internal interface ISolutionSnapshotProviderService { ValueTask RegisterSolutionSnapshotAsync(CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/Copilot/IProposalAdjusterService.cs b/src/Features/Core/Portable/Copilot/IProposalAdjusterService.cs index 1a0f02e62f1e..627d8ba98ca6 100644 --- a/src/Features/Core/Portable/Copilot/IProposalAdjusterService.cs +++ b/src/Features/Core/Portable/Copilot/IProposalAdjusterService.cs @@ -21,7 +21,7 @@ namespace Microsoft.CodeAnalysis.Copilot; -using Adjuster = Func>; +using Adjuster = Func>; internal static class ProposalAdjusterKinds { @@ -46,7 +46,8 @@ internal interface ICopilotProposalAdjusterService : ILanguageService /// default if the proposal was not adjusted ValueTask TryAdjustProposalAsync( ImmutableHashSet allowableAdjustments, Document document, - ImmutableArray normalizedChanges, CancellationToken cancellationToken); + ImmutableArray normalizedChanges, LineFormattingOptions? lineFormattingOptions, + CancellationToken cancellationToken); } internal interface IRemoteCopilotProposalAdjusterService @@ -54,7 +55,8 @@ internal interface IRemoteCopilotProposalAdjusterService /// ValueTask TryAdjustProposalAsync( ImmutableHashSet allowableAdjustments, Checksum solutionChecksum, - DocumentId documentId, ImmutableArray normalizedChanges, CancellationToken cancellationToken); + DocumentId documentId, ImmutableArray normalizedChanges, + LineFormattingOptions? lineFormattingOptions, CancellationToken cancellationToken); } internal abstract class AbstractCopilotProposalAdjusterService : ICopilotProposalAdjusterService @@ -67,9 +69,9 @@ public AbstractCopilotProposalAdjusterService(IGlobalOptionService globalOptions { this.globalOptions = globalOptions; _adjusters = [ - (ProposalAdjusterKinds.AddMissingTokens, this.AddMissingTokensIfAppropriateAsync), - (ProposalAdjusterKinds.AddMissingImports, this.TryGetAddImportTextChangesAsync), - (ProposalAdjusterKinds.FormatCode, this.TryGetFormattingTextChangesAsync), + (ProposalAdjusterKinds.AddMissingTokens, (original, forked, _, ct) => this.AddMissingTokensIfAppropriateAsync(original, forked, ct)), + (ProposalAdjusterKinds.AddMissingImports, static (original, forked, _, ct) => TryGetAddImportTextChangesAsync(original, forked, ct)), + (ProposalAdjusterKinds.FormatCode, static (original, forked, lineFormatting, ct) => TryGetFormattingTextChangesAsync(original, forked, lineFormatting, ct)), ]; } @@ -78,7 +80,8 @@ protected abstract Task AddMissingTokensIfAppropriateAsync( public async ValueTask TryAdjustProposalAsync( ImmutableHashSet allowableAdjustments, Document document, - ImmutableArray normalizedChanges, CancellationToken cancellationToken) + ImmutableArray normalizedChanges, LineFormattingOptions? lineFormattingOptions, + CancellationToken cancellationToken) { if (normalizedChanges.IsDefaultOrEmpty) return default; @@ -89,19 +92,22 @@ public async ValueTask TryAdjustProposalAsync( var result = await client.TryInvokeAsync( document.Project, (service, checksum, cancellationToken) => service.TryAdjustProposalAsync( - allowableAdjustments, checksum, document.Id, normalizedChanges, cancellationToken), + allowableAdjustments, checksum, document.Id, normalizedChanges, + lineFormattingOptions, cancellationToken), cancellationToken).ConfigureAwait(false); return result.HasValue ? result.Value : default; } return await TryAdjustProposalInCurrentProcessAsync( - allowableAdjustments, document, normalizedChanges, cancellationToken).ConfigureAwait(false); + allowableAdjustments, document, normalizedChanges, lineFormattingOptions, + cancellationToken).ConfigureAwait(false); } private async Task TryAdjustProposalInCurrentProcessAsync( ImmutableHashSet allowableAdjustments, Document originalDocument, - ImmutableArray normalizedChanges, CancellationToken cancellationToken) + ImmutableArray normalizedChanges, LineFormattingOptions? lineFormattingOptions, + CancellationToken cancellationToken) { Debug.Assert(allowableAdjustments is not null); @@ -130,7 +136,7 @@ private async Task TryAdjustProposalInCurrentProcessAs continue; var timer = SharedStopwatch.StartNew(); - var adjustedDocument = await adjuster(originalDocument, forkedDocument, cancellationToken).ConfigureAwait(false); + var adjustedDocument = await adjuster(originalDocument, forkedDocument, lineFormattingOptions, cancellationToken).ConfigureAwait(false); if (forkedDocument != adjustedDocument) { adjustmentResults.Add(new(adjusterName, AdjustmentTime: timer.Elapsed)); @@ -147,12 +153,72 @@ private async Task TryAdjustProposalInCurrentProcessAs // Get the final set of changes between the original document and the new document. var allChanges = await forkedDocument.GetTextChangesAsync(originalDocument, cancellationToken).ConfigureAwait(false); - var totalChanges = allChanges.AsImmutableOrEmpty(); + var totalChanges = FixLineEndingBoundaries(oldText, allChanges.AsImmutableOrEmpty()); return new(totalChanges, Format: true, adjustmentResults.ToImmutableAndClear()); } - private async Task TryGetAddImportTextChangesAsync( + /// + /// If replacement text starts with \n adjacent to \r, or ends with \r adjacent to + /// \n, strip the offending character and shrink the span when the original text at the boundary + /// matches the dropped character. + /// + private static ImmutableArray FixLineEndingBoundaries( + SourceText originalText, ImmutableArray changes) + { + if (changes.IsDefaultOrEmpty) + return changes; + + using var _ = ArrayBuilder.GetInstance(out var result); + var anyFixed = false; + + foreach (var change in changes) + { + var span = change.Span; + var newText = change.NewText ?? ""; + var changed = false; + + if (newText.Length > 0) + { + if (newText[0] == '\n' && + span.Start > 0 && + originalText[span.Start - 1] == '\r') + { + // The replacement text would add a \n to a \r, changing the nature of the line break. + if (originalText[span.Start] == '\n') + { + // The \n exists in the original text. There is no reason to replace it. + span = TextSpan.FromBounds(span.Start + 1, Math.Max(span.Start + 1, span.End)); + } + + newText = newText[1..]; + changed = true; + } + + if (newText.Length > 0 && newText[^1] == '\r' && + span.End < originalText.Length && + originalText[span.End] == '\n') + { + // The replacement text would add a \r to a \n, changing the nature of the line break. + if (originalText[span.End - 1] == '\r') + { + // The \r already exists in the original text. There is no reason to replace it. + span = TextSpan.FromBounds(Math.Min(span.Start, span.End - 1), span.End - 1); + } + + newText = newText[..^1]; + changed = true; + } + } + + anyFixed = anyFixed || changed; + result.Add(changed ? new TextChange(span, newText) : change); + } + + return anyFixed ? result.ToImmutableAndClear() : changes; + } + + private static async Task TryGetAddImportTextChangesAsync( Document originalDocument, Document forkedDocument, CancellationToken cancellationToken) { var missingImportsService = originalDocument.GetRequiredLanguageService(); @@ -170,13 +236,19 @@ private async Task TryGetAddImportTextChangesAsync( return withImportsDocument; } - private async Task TryGetFormattingTextChangesAsync( - Document originalDocument, Document forkedDocument, CancellationToken cancellationToken) + private static async Task TryGetFormattingTextChangesAsync( + Document originalDocument, Document forkedDocument, + LineFormattingOptions? lineFormattingOptions, CancellationToken cancellationToken) { var syntaxFormattingService = originalDocument.GetRequiredLanguageService(); var formattingOptions = await originalDocument.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false); + // Override with the buffer-derived line formatting options if available, so the formatter + // uses the file's actual newline character and inferred indentation settings. + if (lineFormattingOptions is not null) + formattingOptions = formattingOptions with { LineFormatting = lineFormattingOptions }; + // Find the span of changes made to the forked document. var totalNewSpan = await GetSpanOfChangesAsync(originalDocument, forkedDocument, cancellationToken).ConfigureAwait(false); @@ -215,4 +287,11 @@ private static async Task GetSpanOfChangesAsync(Document oldDocument, var totalNewSpan = GetSpanToAnalyze(forkedRoot, totalSpans); return totalNewSpan; } + + internal readonly struct TestAccessor + { + internal static ImmutableArray FixLineEndingBoundaries( + SourceText originalText, ImmutableArray changes) + => AbstractCopilotProposalAdjusterService.FixLineEndingBoundaries(originalText, changes); + } } diff --git a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs index 295df004e976..848cc137b0ce 100644 --- a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs @@ -93,6 +93,10 @@ Task> GetDiagnosticsForSpanAsync( DiagnosticKind diagnosticKind, CancellationToken cancellationToken); + /// + Task>> GetAllDiagnosticIdsAsync( + Solution solution, ImmutableArray projectIds, CancellationToken cancellationToken); + /// Task>> GetDiagnosticDescriptorsPerReferenceAsync( Solution solution, ProjectId? projectId, CancellationToken cancellationToken); diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_GetDiagnosticsForSpan.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_GetDiagnosticsForSpan.cs index 5929a919f7f2..0460e90917d0 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_GetDiagnosticsForSpan.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_GetDiagnosticsForSpan.cs @@ -294,17 +294,23 @@ private async Task> ComputeDiagnosticsInProcessAs using var _ = ArrayBuilder.GetInstance(out var list); - await ComputeDocumentDiagnosticsAsync(syntaxAnalyzers, AnalysisKind.Syntax, range, incrementalAnalysis: false).ConfigureAwait(false); - await ComputeDocumentDiagnosticsAsync(semanticSpanAnalyzers, AnalysisKind.Semantic, range, incrementalAnalysis).ConfigureAwait(false); - await ComputeDocumentDiagnosticsAsync(semanticDocumentAnalyzers, AnalysisKind.Semantic, span: null, incrementalAnalysis: false).ConfigureAwait(false); + await ComputeDocumentDiagnosticsAsync(this, document, compilationWithAnalyzers, logPerformanceInfo, syntaxAnalyzers, AnalysisKind.Syntax, range, incrementalAnalysis: false, list, cancellationToken).ConfigureAwait(false); + await ComputeDocumentDiagnosticsAsync(this, document, compilationWithAnalyzers, logPerformanceInfo, semanticSpanAnalyzers, AnalysisKind.Semantic, range, incrementalAnalysis, list, cancellationToken).ConfigureAwait(false); + await ComputeDocumentDiagnosticsAsync(this, document, compilationWithAnalyzers, logPerformanceInfo, semanticDocumentAnalyzers, AnalysisKind.Semantic, span: null, incrementalAnalysis: false, list, cancellationToken).ConfigureAwait(false); return list.ToImmutableAndClear(); - async Task ComputeDocumentDiagnosticsAsync( + static async Task ComputeDocumentDiagnosticsAsync( + DiagnosticAnalyzerService service, + TextDocument document, + CompilationWithAnalyzers? compilationWithAnalyzers, + bool logPerformanceInfo, ImmutableArray analyzers, AnalysisKind kind, TextSpan? span, - bool incrementalAnalysis) + bool incrementalAnalysis, + ArrayBuilder list, + CancellationToken cancellationToken) { if (analyzers.Length == 0) return; @@ -313,16 +319,16 @@ async Task ComputeDocumentDiagnosticsAsync( Debug.Assert(!incrementalAnalysis || analyzers.All(analyzer => analyzer.SupportsSpanBasedSemanticDiagnosticAnalysis())); var analysisScope = new DocumentAnalysisScope(document, span, analyzers, kind); - var executor = new DocumentAnalysisExecutor(this, analysisScope, compilationWithAnalyzers, logPerformanceInfo); + var executor = new DocumentAnalysisExecutor(service, analysisScope, compilationWithAnalyzers, logPerformanceInfo); var version = await GetDiagnosticVersionAsync(document.Project, cancellationToken).ConfigureAwait(false); var computeTask = incrementalAnalysis - ? _incrementalMemberEditAnalyzer.ComputeDiagnosticsInProcessAsync(executor, analyzers, version, cancellationToken) + ? service._incrementalMemberEditAnalyzer.ComputeDiagnosticsInProcessAsync(executor, analyzers, version, cancellationToken) : ComputeDocumentDiagnosticsCoreInProcessAsync(executor, cancellationToken); var diagnosticsMap = await computeTask.ConfigureAwait(false); if (incrementalAnalysis) - _incrementalMemberEditAnalyzer.UpdateDocumentWithCachedDiagnostics((Document)document); + service._incrementalMemberEditAnalyzer.UpdateDocumentWithCachedDiagnostics((Document)document); list.AddRange(diagnosticsMap.SelectMany(kvp => kvp.Value)); } diff --git a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_RemoteOrLocalDispatcher.cs b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_RemoteOrLocalDispatcher.cs index 19393e6c24cc..d9f976598419 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_RemoteOrLocalDispatcher.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DiagnosticAnalyzerService_RemoteOrLocalDispatcher.cs @@ -5,12 +5,12 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Diagnostics; @@ -102,6 +102,32 @@ public async Task> GetCompilationEndDiagnosticDescriptorI return builder.ToImmutableArray(); } + public async Task>> GetAllDiagnosticIdsAsync( + Solution solution, ImmutableArray projectIds, CancellationToken cancellationToken) + { + var client = await RemoteHostClient.TryGetClientAsync(solution.Services, cancellationToken).ConfigureAwait(false); + if (client is not null) + { + var list = await client.TryInvokeAsync>>( + solution, + (service, solution, cancellationToken) => service.GetAllDiagnosticIdsAsync( + solution, projectIds, cancellationToken), + cancellationToken).ConfigureAwait(false); + if (!list.HasValue) + return []; + + return list.Value; + } + + var builder = ImmutableArray.CreateBuilder(); + + foreach (var projectId in projectIds) + builder.Add(solution.GetRequiredProject(projectId)); + + return solution.SolutionState.Analyzers.GetAllDiagnosticIds( + this._analyzerInfoCache, builder.ToImmutable()); + } + public async Task>> GetDiagnosticDescriptorsPerReferenceAsync( Solution solution, ProjectId? projectId, CancellationToken cancellationToken) { diff --git a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs index 8e5c85286f61..bde1b3f2d1f7 100644 --- a/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs +++ b/src/Features/Core/Portable/Diagnostics/Service/DocumentAnalysisExecutor.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Telemetry; using Microsoft.CodeAnalysis.Text; @@ -50,10 +51,18 @@ public DocumentAnalysisExecutor( _logPerformanceInfo = logPerformanceInfo; _onAnalysisException = onAnalysisException; - var compilationBasedProjectAnalyzers = compilationWithAnalyzers?.Analyzers.ToImmutableHashSet(); - _compilationBasedAnalyzersInAnalysisScope = compilationBasedProjectAnalyzers != null - ? analysisScope.Analyzers.WhereAsArray(compilationBasedProjectAnalyzers.Contains) - : []; + if (compilationWithAnalyzers is null || compilationWithAnalyzers.Analyzers.IsDefaultOrEmpty) + { + _compilationBasedAnalyzersInAnalysisScope = []; + } + else + { + using var _ = PooledHashSet.GetInstance(out var compilationBasedProjectAnalyzers); + + compilationBasedProjectAnalyzers.AddRange(compilationWithAnalyzers.Analyzers); + + _compilationBasedAnalyzersInAnalysisScope = analysisScope.Analyzers.WhereAsArray(compilationBasedProjectAnalyzers.Contains); + } } public DocumentAnalysisScope AnalysisScope { get; } diff --git a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs index 6a398fe7223a..79cbbf0967d5 100644 --- a/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs +++ b/src/Features/Core/Portable/EditAndContinue/AbstractEditAndContinueAnalyzer.cs @@ -1872,7 +1872,7 @@ internal TextSpan GetDeletedDeclarationActiveSpan(IReadOnlyDictionary + /// Try to get ahold of source code snapshot that matches the content of the source file when the compiler read it during build. + /// This is not always possible since the file on disk can be changed from outside of the IDE at any point in time, before we have + /// the opportunity to capture it. + /// + /// Possible improvements: + /// 1) check if the PDB contains embedded source for the document (https://github.com/dotnet/roslyn/issues/82879) + /// 2) send request to VBCSCompiler for the content of the file; if the project was just built it might still be loaded in the server (https://github.com/dotnet/sdk/issues/53550) + /// + /// + /// dotnet-watch captures the content of all source files when the session starts. Such approach would be too slow for the IDE. + /// It's necessary for dotnet-watch since watch is entirely dependent on watching file system changes and it does not have any other way to find + /// the baseline content of a modified source file. Although it could use [1] and [2] above, these are not always available. + /// + /// In the IDE we prefer to not block project launching on hydrating the content of all source files since we don't even know + /// whether the user intends to use Hot Reload or not. In fact, in most cases the user does not make any changes and just wants to run or debug an app. + /// Unlike dotnet-watch, which is explicitly used for Hot Reload and capturing the source content is part of project loading. + /// private static async ValueTask> TryGetMatchingSourceTextAsync( TraceLog log, SourceText sourceText, @@ -372,6 +390,11 @@ public bool ContainsDocument(DocumentId documentId) var text = await sourceTextProvider.TryGetMatchingSourceTextAsync(filePath, requiredChecksum, checksumAlgorithm, cancellationToken).ConfigureAwait(false); if (text != null) { + // Note: the encoding and the checksum of the resulting text does not need to be correct, + // since the provider already verified that the decoded text string matches the checksum in the PDB. + // If we needed it to be exact for some reason we would need to update TryGetMatchingSourceTextAsync + // to return SourceText (tracked https://github.com/dotnet/roslyn/issues/64504). We might want do that + // for perf reasons to avoid transfering large strings OOP. return SourceText.From(text, defaultEncoding, checksumAlgorithm); } diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs index a8989879c251..19226e9d1e09 100644 --- a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs @@ -274,7 +274,8 @@ private bool AddModulePreparedForUpdate(Guid mvid) /// internal Task<(Guid Mvid, Diagnostic? Error)> GetProjectModuleIdAsync(Project project, CancellationToken cancellationToken) { - Debug.Assert(project.SupportsEditAndContinue()); + Debug.Assert(!project.IgnoreForEditAndContinue()); + // Note: Does not cache the result as the project may be rebuilt at any point in time. return Task.Run(ReadMvid, cancellationToken); diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs index 6790b42ebf1f..becb77afcce7 100644 --- a/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs +++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs @@ -110,7 +110,8 @@ public static void Log(Data data, Action log, Func // all rude edits (errors and warnings) map["RudeEditsCount"] = editSessionData.RudeEdits.Length; - // Number of emit errors. These are any errors only produced during emitting deltas and do not include document analysis errors. + // Emit errors. These are any errors only produced during emitting deltas and do not include document analysis errors. + // Includes compiler errors and unsupported project updates. map["EmitDeltaErrorIdCount"] = editSessionData.EmitErrorIds.Length; // False for Hot Reload session, true or missing for EnC session (missing in older data that did not have this property). diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs index 18267122c1c5..f4ba65bc5e73 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDiagnosticDescriptors.cs @@ -165,8 +165,8 @@ void AddProjectRudeEdit(ProjectSettingKind kind) AddRudeEdit(RudeEditKind.ChangingConstraints, nameof(FeaturesResources.Changing_constraints_of_0_requires_restarting_the_application)); AddRudeEdit(RudeEditKind.ChangeImplicitMainReturnType, nameof(FeaturesResources.An_update_that_causes_the_return_type_of_implicit_main_to_change_requires_restarting_the_application)); AddRudeEdit(RudeEditKind.RenamingNotSupportedByRuntime, nameof(FeaturesResources.Renaming_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); - AddRudeEdit(RudeEditKind.ChangingNonCustomAttribute, nameof(FeaturesResources.Changing_pseudo_custom_attribute_0_of_1_requires_restarting_th_application)); - AddRudeEdit(RudeEditKind.ChangingNamespace, nameof(FeaturesResources.Changing_the_containing_namespace_of_0_from_1_to_2_requires_restarting_th_application)); + AddRudeEdit(RudeEditKind.ChangingNonCustomAttribute, nameof(FeaturesResources.Changing_pseudo_custom_attribute_0_of_1_requires_restarting_the_application)); + AddRudeEdit(RudeEditKind.ChangingNamespace, nameof(FeaturesResources.Changing_the_containing_namespace_of_0_from_1_to_2_requires_restarting_the_application)); AddRudeEdit(RudeEditKind.ChangingSignatureNotSupportedByRuntime, nameof(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime)); AddRudeEdit(RudeEditKind.DeleteNotSupportedByRuntime, nameof(FeaturesResources.Deleting_0_requires_restarting_the_application_because_is_not_supported_by_the_runtime)); AddRudeEdit(RudeEditKind.UpdatingStateMachineMethodNotSupportedByRuntime, nameof(FeaturesResources.Updating_async_or_iterator_requires_restarting_the_application_because_is_not_supported_by_the_runtime)); @@ -198,6 +198,7 @@ void AddProjectRudeEdit(ProjectSettingKind kind) AddGeneralDiagnostic(EditAndContinueErrorCode.UnableToReadSourceFileOrPdb, nameof(FeaturesResources.UnableToReadSourceFileOrPdb)); AddGeneralDiagnostic(EditAndContinueErrorCode.AddingTypeRuntimeCapabilityRequired, nameof(FeaturesResources.ChangesRequiredSynthesizedType)); AddGeneralDiagnostic(EditAndContinueErrorCode.UpdatingDocumentInStaleProject, nameof(FeaturesResources.Changing_source_file_0_in_a_stale_project_1_has_no_effect_until_the_project_is_rebuilt_2), DiagnosticSeverity.Warning, noEffect: true); + AddGeneralDiagnostic(EditAndContinueErrorCode.UpdatingUnsupportedProject, nameof(FeaturesResources.Changing_source_file_0_in_a_project_1_that_does_not_support_hot_reload_requires_restarting_the_application_2)); // Project setting rude edits. Defines a distinct error code per setting to simplify telemetry tracking even though some errors share the same message. diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs index 481f46e5bb79..97b04afcd4a2 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueErrorCode.cs @@ -14,6 +14,7 @@ internal enum EditAndContinueErrorCode UnableToReadSourceFileOrPdb = 6, AddingTypeRuntimeCapabilityRequired = 7, UpdatingDocumentInStaleProject = 8, + UpdatingUnsupportedProject = 9, ChangingMultiVersionReferences = 98, ChangingReference = 99, diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs index f87762dd3027..e66afee21a0a 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs @@ -135,9 +135,9 @@ internal static async ValueTask HydrateDocumentsAsync(Solution solution, Cancell { var documentTasks = from project in solution.Projects - where project.SupportsEditAndContinue() + where !project.IgnoreForEditAndContinue() from documentState in GetDocumentStates(project.State) - where documentState.SupportsEditAndContinue() + where !documentState.IgnoreForEditAndContinue() select documentState.GetTextAsync(cancellationToken).AsTask(); _ = await Task.WhenAll(documentTasks).ConfigureAwait(false); diff --git a/src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueSessionState.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueSessionState.cs similarity index 100% rename from src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueSessionState.cs rename to src/Features/Core/Portable/EditAndContinue/EditAndContinueSessionState.cs diff --git a/src/Features/Core/Portable/EditAndContinue/EditSession.cs b/src/Features/Core/Portable/EditAndContinue/EditSession.cs index c318b3e6addc..d1397d743862 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditSession.cs @@ -16,7 +16,6 @@ using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -284,7 +283,7 @@ public static async ValueTask HasChangesAsync(Solution oldSolution, Soluti foreach (var newProject in newSolution.Projects) { - if (!newProject.SupportsEditAndContinue()) + if (newProject.IgnoreForEditAndContinue()) { continue; } @@ -299,7 +298,7 @@ public static async ValueTask HasChangesAsync(Solution oldSolution, Soluti foreach (var oldProject in oldSolution.Projects) { - if (!oldProject.SupportsEditAndContinue()) + if (oldProject.IgnoreForEditAndContinue()) { continue; } @@ -333,10 +332,8 @@ private static async ValueTask ContentEqualsAsync(TextDocument oldDocument internal static async ValueTask HasDifferencesAsync(Project oldProject, Project newProject, ProjectDifferences? differences, CancellationToken cancellationToken) { - if (!newProject.SupportsEditAndContinue()) - { - return false; - } + Debug.Assert(!oldProject.IgnoreForEditAndContinue()); + Debug.Assert(!newProject.IgnoreForEditAndContinue()); if (oldProject.State == newProject.State) { @@ -351,7 +348,7 @@ internal static async ValueTask HasDifferencesAsync(Project oldProject, Pr foreach (var documentId in newProject.State.DocumentStates.GetChangedStateIds(oldProject.State.DocumentStates, ignoreUnchangedContent: true)) { var document = newProject.GetRequiredDocument(documentId); - if (!document.State.SupportsEditAndContinue()) + if (document.State.IgnoreForEditAndContinue()) { continue; } @@ -372,7 +369,7 @@ internal static async ValueTask HasDifferencesAsync(Project oldProject, Pr foreach (var documentId in newProject.State.DocumentStates.GetAddedStateIds(oldProject.State.DocumentStates)) { var document = newProject.GetRequiredDocument(documentId); - if (!document.State.SupportsEditAndContinue()) + if (document.State.IgnoreForEditAndContinue()) { continue; } @@ -388,7 +385,7 @@ internal static async ValueTask HasDifferencesAsync(Project oldProject, Pr foreach (var documentId in newProject.State.DocumentStates.GetRemovedStateIds(oldProject.State.DocumentStates)) { var document = oldProject.GetRequiredDocument(documentId); - if (!document.State.SupportsEditAndContinue()) + if (document.State.IgnoreForEditAndContinue()) { continue; } @@ -447,8 +444,15 @@ internal static async ValueTask HasDifferencesAsync(Project oldProject, Pr /// internal static bool HasProjectLevelDifferences(Project oldProject, Project newProject, ProjectDifferences? differences) { - Debug.Assert(oldProject.CompilationOptions != null); - Debug.Assert(newProject.CompilationOptions != null); + if (oldProject.CompilationOptions == null || oldProject.ParseOptions == null) + { + Contract.ThrowIfFalse(newProject.CompilationOptions == null); + Contract.ThrowIfFalse(newProject.ParseOptions == null); + return false; + } + + Contract.ThrowIfNull(newProject.CompilationOptions); + Contract.ThrowIfNull(newProject.ParseOptions); if (oldProject.ParseOptions != newProject.ParseOptions || HasDifferences(oldProject.CompilationOptions, newProject.CompilationOptions) || @@ -611,6 +615,7 @@ internal static async IAsyncEnumerable GetChangedDocumentsAsync(Trac ProjectDifferences differences, ActiveStatementSpanProvider newDocumentActiveStatementSpanProvider, ArrayBuilder diagnostics, + bool projectSupportsEditAndContinue, CancellationToken cancellationToken) { using var _ = ArrayBuilder<(Document? oldDocument, Document? newDocument)>.GetInstance(out var documents); @@ -653,11 +658,38 @@ internal static async IAsyncEnumerable GetChangedDocumentsAsync(Trac documents.Add((oldDocument, newDocument: null)); } - // No need to report rude edits if project has any documents that are out of sync. No deltas will be emitted for such project. - var analyses = staleDocument != null - ? [] - : await Analyses.GetDocumentAnalysesAsync(DebuggingSession.LastCommittedSolution, newSolution, documents, newDocumentActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false); + if (staleDocument != null) + { + return ([], staleDocument); + } + + if (!projectSupportsEditAndContinue) + { + // Bail early for projects that do not support EnC. + // If the document source is stale the detected changes might not be accurate. + // If this becomes an issue we'll need to ensure we have compilation outputs settings for the project, so that we can read the PDB. + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.UpdatingUnsupportedProject); + string? reason = null; + foreach (var (oldDocument, newDocument) in documents) + { + var document = newDocument ?? oldDocument; + Contract.ThrowIfNull(document); + Contract.ThrowIfNull(document.FilePath); + + reason ??= string.Format(FeaturesResources._0_does_not_support_Hot_Reload, document.Project.Language); + + diagnostics.Add(Diagnostic.Create( + descriptor, + Location.Create(document.FilePath, textSpan: default, lineSpan: default), + [document.Name, document.Project.Name, reason])); + } + + return ([], staleDocument: null); + } + + // No need to report rude edits if project has any documents that are out of sync. No deltas will be emitted for such project. + var analyses = await Analyses.GetDocumentAnalysesAsync(DebuggingSession.LastCommittedSolution, newSolution, documents, newDocumentActiveStatementSpanProvider, cancellationToken).ConfigureAwait(false); return (analyses, staleDocument); } @@ -1107,13 +1139,22 @@ void UpdateChangedDocumentsStaleness(DocumentStalenessReason? staleness) { try { - if (!newProject.SupportsEditAndContinue(Log)) + if (newProject.IgnoreForEditAndContinue(Log)) { continue; } var oldProject = oldSolution.GetProject(newProject.Id); - Debug.Assert(oldProject == null || oldProject.SupportsEditAndContinue()); + if (oldProject?.IgnoreForEditAndContinue(Log) == true) + { + continue; + } + + var projectSupportsEditAndContinue = newProject.SupportsEditAndContinue(Log); + if (oldProject != null && projectSupportsEditAndContinue != oldProject.SupportsEditAndContinue()) + { + continue; + } await GetProjectDifferencesAsync(Log, oldProject, newProject, projectDifferences, projectDiagnostics, cancellationToken).ConfigureAwait(false); projectDifferences.Log(Log, newProject); @@ -1189,7 +1230,7 @@ void UpdateChangedDocumentsStaleness(DocumentStalenessReason? staleness) // instead of the true C.M(string). var (changedDocumentAnalyses, staleDocument) = - await AnalyzeProjectDifferencesAsync(solution, projectDifferences, solutionActiveStatementSpanProvider, projectDiagnostics, cancellationToken).ConfigureAwait(false); + await AnalyzeProjectDifferencesAsync(solution, projectDifferences, solutionActiveStatementSpanProvider, projectDiagnostics, projectSupportsEditAndContinue, cancellationToken).ConfigureAwait(false); if (staleDocument != null) { @@ -1227,6 +1268,11 @@ void UpdateChangedDocumentsStaleness(DocumentStalenessReason? staleness) Telemetry.LogAnalysisTime(changedDocumentAnalysis.ElapsedTime); } + if (!projectSupportsEditAndContinue) + { + continue; + } + var projectSummary = GetProjectAnalysisSummary(changedDocumentAnalyses); if (HasProjectSettingsBlockingRudeEdits(oldProject, newProject, projectDiagnostics)) diff --git a/src/Features/Core/Portable/EditAndContinue/EditSessionTelemetry.cs b/src/Features/Core/Portable/EditAndContinue/EditSessionTelemetry.cs index 44b2d7143975..135435e0ce97 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditSessionTelemetry.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditSessionTelemetry.cs @@ -73,7 +73,7 @@ public Data GetDataAndClear() } } - public bool IsEmpty => !(_hadSyntaxErrors || _hadBlockingRudeEdits || _hadValidChanges || _hadValidInsignificantChanges); + public bool IsEmpty => !(_hadSyntaxErrors || _hadBlockingRudeEdits || _hadValidChanges || _hadValidInsignificantChanges || _emitErrorIds.Count > 0); public void SetBreakState(bool value) => _inBreakState = value; diff --git a/src/Features/Core/Portable/EditAndContinue/IActiveStatementTrackingController.cs b/src/Features/Core/Portable/EditAndContinue/IActiveStatementTrackingController.cs new file mode 100644 index 000000000000..750656c2ab59 --- /dev/null +++ b/src/Features/Core/Portable/EditAndContinue/IActiveStatementTrackingController.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal interface IActiveStatementTrackingController +{ + void StartTracking(Solution solution, IActiveStatementSpanFactory spanProvider); + + void EndTracking(); + + ActiveStatementSpanProvider GetSpanProvider(Solution solution); +} diff --git a/src/EditorFeatures/Core/EditAndContinue/IEditAndContinueSolutionProvider.cs b/src/Features/Core/Portable/EditAndContinue/IEditAndContinueSolutionProvider.cs similarity index 100% rename from src/EditorFeatures/Core/EditAndContinue/IEditAndContinueSolutionProvider.cs rename to src/Features/Core/Portable/EditAndContinue/IEditAndContinueSolutionProvider.cs diff --git a/src/Features/Core/Portable/EditAndContinue/IPdbMatchingSourceTextProvider.cs b/src/Features/Core/Portable/EditAndContinue/IPdbMatchingSourceTextProvider.cs index 35f21c48e4ad..d788dd157921 100644 --- a/src/Features/Core/Portable/EditAndContinue/IPdbMatchingSourceTextProvider.cs +++ b/src/Features/Core/Portable/EditAndContinue/IPdbMatchingSourceTextProvider.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.EditAndContinue; diff --git a/src/Features/Core/Portable/EditAndContinue/ISolutionSnapshotProvider.cs b/src/Features/Core/Portable/EditAndContinue/ISolutionSnapshotProvider.cs new file mode 100644 index 000000000000..a07f03658e07 --- /dev/null +++ b/src/Features/Core/Portable/EditAndContinue/ISolutionSnapshotProvider.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal interface ISolutionSnapshotProvider +{ + ValueTask GetCurrentSolutionAsync(CancellationToken cancellationToken); +} diff --git a/src/EditorFeatures/Core/EditAndContinue/PdbMatchingSourceTextProvider.cs b/src/Features/Core/Portable/EditAndContinue/PdbMatchingSourceTextProvider.cs similarity index 88% rename from src/EditorFeatures/Core/EditAndContinue/PdbMatchingSourceTextProvider.cs rename to src/Features/Core/Portable/EditAndContinue/PdbMatchingSourceTextProvider.cs index 7601f6ad5249..bd8c2a58d428 100644 --- a/src/EditorFeatures/Core/EditAndContinue/PdbMatchingSourceTextProvider.cs +++ b/src/Features/Core/Portable/EditAndContinue/PdbMatchingSourceTextProvider.cs @@ -18,7 +18,9 @@ namespace Microsoft.CodeAnalysis.EditAndContinue; /// -/// Notifies EnC service of host workspace events. +/// Captures of documents that transition from being backed by to being backed by text buffer when a document is opened in the editor. +/// Gives us an opportunity to observe the version of the source text that matches the one used to produce the PDB. After the document is opened the content can be updated in-memory (in the editor), +/// saved to disk and the version that matches the PDB lost. /// [ExportEventListener(WellKnownEventListeners.Workspace, WorkspaceKind.Host), Shared] [Export(typeof(PdbMatchingSourceTextProvider))] @@ -35,6 +37,12 @@ internal sealed class PdbMatchingSourceTextProvider() : IEventListener, IPdbMatc public void StartListening(Workspace workspace) { + // TODO: Workaround for LSP tests creating two Host workspaces. https://github.com/dotnet/roslyn/issues/82917 + if (workspace.GetType().Name == "LspTestWorkspace") + { + return; + } + Debug.Assert(_workspaceChangedDisposer == null); _workspaceChangedDisposer = workspace.RegisterWorkspaceChangedHandler(WorkspaceChanged); diff --git a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs b/src/Features/Core/Portable/EditAndContinue/Remote/DebuggingSessionProxy.cs similarity index 96% rename from src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs rename to src/Features/Core/Portable/EditAndContinue/Remote/DebuggingSessionProxy.cs index a789e81577fd..a71726bf3bbe 100644 --- a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteDebuggingSessionProxy.cs +++ b/src/Features/Core/Portable/EditAndContinue/Remote/DebuggingSessionProxy.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.EditAndContinue; -internal sealed class RemoteDebuggingSessionProxy(SolutionServices services, IDisposable? connection, DebuggingSessionId sessionId) : IActiveStatementSpanFactory, IDisposable +internal sealed class DebuggingSessionProxy(SolutionServices services, IDisposable? connection, DebuggingSessionId sessionId) : IActiveStatementSpanFactory, IDisposable { public void Dispose() => connection?.Dispose(); @@ -78,7 +78,7 @@ await client.TryInvokeAsync( } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - return EmitSolutionUpdateResults.Data.CreateFromInternalError(solution, e.Message, runningProjects); + return EmitSolutionUpdateResults.Data.CreateFromInternalError(solution, e.ToString(), runningProjects); } } diff --git a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteEditAndContinueServiceProxy.cs b/src/Features/Core/Portable/EditAndContinue/Remote/RemoteEditAndContinueServiceProxy.cs index 32b513fb6a7d..2924558ef355 100644 --- a/src/Features/Core/Portable/EditAndContinue/Remote/RemoteEditAndContinueServiceProxy.cs +++ b/src/Features/Core/Portable/EditAndContinue/Remote/RemoteEditAndContinueServiceProxy.cs @@ -117,7 +117,7 @@ public async ValueTask> GetCapabilitiesAsync(Cancellation private IEditAndContinueService GetLocalService() => services.GetRequiredService().Service; - public async ValueTask StartDebuggingSessionAsync( + public async ValueTask StartDebuggingSessionAsync( Solution solution, IManagedHotReloadService debuggerService, IPdbMatchingSourceTextProvider sourceTextProvider, @@ -128,7 +128,7 @@ private IEditAndContinueService GetLocalService() if (client == null) { var sessionId = GetLocalService().StartDebuggingSession(solution, debuggerService, sourceTextProvider, reportDiagnostics); - return new RemoteDebuggingSessionProxy(solution.Services, LocalConnection.Instance, sessionId); + return new DebuggingSessionProxy(solution.Services, LocalConnection.Instance, sessionId); } // need to keep the providers alive until the session ends: @@ -142,7 +142,7 @@ private IEditAndContinueService GetLocalService() if (sessionIdOpt.HasValue) { - return new RemoteDebuggingSessionProxy(solution.Services, connection, sessionIdOpt.Value); + return new DebuggingSessionProxy(solution.Services, connection, sessionIdOpt.Value); } connection.Dispose(); diff --git a/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs b/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs index 479b8ce2a567..0ccda529ea70 100644 --- a/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs +++ b/src/Features/Core/Portable/EditAndContinue/Utilities/Extensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.Contracts.EditAndContinue; @@ -43,9 +44,9 @@ public static SourceSpan ToSourceSpan(this LinePositionSpan span) /// public static bool SupportsEditAndContinue(this Project project, TraceLog? log = null) { - if (project.FilePath == null) + if (project.IgnoreForEditAndContinue()) { - LogReason("no file path"); + LogReason("invalid file path"); return false; } @@ -61,58 +62,86 @@ public static bool SupportsEditAndContinue(this Project project, TraceLog? log = return false; } - if (!project.CompilationOutputInfo.HasEffectiveGeneratedFilesOutputDirectory) + void LogReason(string message) + => log?.Write($"Project '{project.GetLogDisplay()}' doesn't support EnC: {message}"); + + return true; + } + + /// + /// True if the project should be ignore for the purposes of Edit and Continue. + /// These are synthesized or misconfigured projects that don't produce an assembly. + /// + public static bool IgnoreForEditAndContinue(this Project project, TraceLog? log = null) + { + if (!PathUtilities.IsAbsolute(project.FilePath)) + { + LogReason("invalid file path"); + return true; + } + + if (project.SupportsCompilation && !project.CompilationOutputInfo.HasEffectiveGeneratedFilesOutputDirectory) { LogReason("no generated files output directory"); - return false; + return true; } void LogReason(string message) - => log?.Write($"Project '{project.GetLogDisplay()}' doesn't support EnC: {message}"); + => log?.Write($"Project {project.GetLogDisplay()} ignored for EnC: {message}"); - return true; + return false; } public static string GetLogDisplay(this Project project) => project.FilePath != null ? $"'{project.FilePath}'" + (project.State.NameAndFlavor.flavor is { } flavor ? $" ('{flavor}')" : "") - : $"'{project.Name}' ('{project.Id.DebugName}'"; + : $"'{project.Name}' ('{project.Id.DebugName}')"; public static bool SupportsEditAndContinue(this TextDocumentState textDocumentState) { - if (textDocumentState.Attributes.DesignTimeOnly) + if (textDocumentState.IgnoreForEditAndContinue()) { return false; } - if (!PathUtilities.IsAbsolute(textDocumentState.FilePath)) + if (textDocumentState is DocumentState { SupportsSyntaxTree: false }) { return false; } - if (textDocumentState is DocumentState documentState) + return true; + } + + public static bool IgnoreForEditAndContinue(this TextDocumentState textDocumentState) + { + if (textDocumentState.Attributes.DesignTimeOnly) { - if (!documentState.SupportsSyntaxTree) - { - return false; - } + return true; + } + if (!PathUtilities.IsAbsolute(textDocumentState.FilePath)) + { + return true; + } + + if (textDocumentState is DocumentState documentState) + { // WPF design time documents are added to the Workspace by the Project System as regular documents, // although they are not compiled into the binary. if (IsWpfDesignTimeOnlyDocument(textDocumentState.FilePath, documentState.LanguageServices.Language)) { - return false; + return true; } // Razor generated documents are added to the Workspace by the Web Tools editor but aren't used at runtime, // so don't need to be considered for edit and continue. - if (IsRazorDesignTimeOnlyDocument(textDocumentState.FilePath)) + if (IsRazorDesignTimeOnlyDocument(textDocumentState.FilePath, documentState.LanguageServices.Language)) { - return false; + return true; } } - return true; + return false; } private static bool IsWpfDesignTimeOnlyDocument(string filePath, string language) @@ -123,9 +152,14 @@ private static bool IsWpfDesignTimeOnlyDocument(string filePath, string language _ => false }; - private static bool IsRazorDesignTimeOnlyDocument(string filePath) - => filePath.EndsWith(".razor.g.cs", StringComparison.OrdinalIgnoreCase) || - filePath.EndsWith(".cshtml.g.cs", StringComparison.OrdinalIgnoreCase); + private static bool IsRazorDesignTimeOnlyDocument(string filePath, string language) + => language switch + { + LanguageNames.CSharp => + filePath.EndsWith(".razor.g.cs", StringComparison.OrdinalIgnoreCase) || + filePath.EndsWith(".cshtml.g.cs", StringComparison.OrdinalIgnoreCase), + _ => false + }; public static ManagedHotReloadDiagnostic ToHotReloadDiagnostic(this DiagnosticData data, ManagedHotReloadDiagnosticSeverity severity) { diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/IStackFrameNodeVisitor.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/IStackFrameNodeVisitor.cs index 6fcec8418a7b..74ce5cc2452e 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/IStackFrameNodeVisitor.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/IStackFrameNodeVisitor.cs @@ -20,4 +20,5 @@ internal interface IStackFrameNodeVisitor void Visit(StackFrameGeneratedMethodNameNode stackFrameGeneratedNameNode); void Visit(StackFrameLocalMethodNameNode stackFrameLocalMethodNameNode); void Visit(StackFrameConstructorNode constructorNode); + void Visit(StackFrameStateMachineMethodNameNode stackFrameStateMachineMethodNameNode); } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameKind.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameKind.cs index a8bfb5b13ffc..2372620c9099 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameKind.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameKind.cs @@ -16,6 +16,7 @@ internal enum StackFrameKind GenericTypeIdentifier, GeneratedIdentifier, LocalMethodIdentifier, + StateMachineMethodIdentifier, TypeArgument, TypeIdentifier, Parameter, diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameLexer.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameLexer.cs index 7356b6342ba6..1d9879b88d51 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameLexer.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameLexer.cs @@ -323,9 +323,10 @@ public Result TryScanPath() /// /// Scans a form similar to g__, where g is a GeneratedNameKind (a single character) - /// and identifier is valid identifier characters as with + /// and identifier is valid identifier characters as with . + /// If is true, it will also scan for a numeric suffix after "__" /// - public Result TryScanRequiredGeneratedNameSeparator() + public Result TryScanRequiredGeneratedNameSeparator(bool scanNumericsAfter = false) { var start = Position; if (IsAsciiAlphaCharacter(CurrentChar)) @@ -346,6 +347,14 @@ public Result TryScanRequiredGeneratedNameSeparator() return Result.Abort; } + if (scanNumericsAfter) + { + while (IsNumber(CurrentChar)) + { + Position++; + } + } + return CreateToken(StackFrameKind.GeneratedNameSeparatorToken, GetSubSequenceToCurrentPos(start)); } diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs index de10fa0887bc..8e8df485f554 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameNodeDefinitions.cs @@ -205,16 +205,17 @@ protected StackFrameGeneratedNameNode(StackFrameToken identifier, StackFrameKind } /// -/// Generated methods follow the pattern Namespace.ClassName.>MethodName$<(), where -/// the "$" is optional. +/// Generated methods follow the pattern Namespace.ClassName.>MethodName$<Suffix(), where +/// the "$" and "Suffix" are optional. /// internal sealed class StackFrameGeneratedMethodNameNode : StackFrameGeneratedNameNode { public readonly StackFrameToken LessThanToken; public readonly StackFrameToken GreaterThanToken; public readonly StackFrameToken? DollarToken; + public readonly StackFrameToken? Suffix; - internal override int ChildCount => 4; + internal override int ChildCount => 5; public StackFrameGeneratedMethodNameNode(StackFrameToken lessThanToken, StackFrameToken identifier, StackFrameToken greaterThanToken, StackFrameToken? dollarToken) : base(identifier, StackFrameKind.GeneratedIdentifier) @@ -238,6 +239,7 @@ internal override StackFrameNodeOrToken ChildAt(int index) 1 => Identifier, 2 => GreaterThanToken, 3 => DollarToken.HasValue ? DollarToken.Value : null, + 4 => Suffix.HasValue ? Suffix.Value : null, _ => throw new InvalidOperationException() }; } @@ -296,6 +298,44 @@ internal override StackFrameNodeOrToken ChildAt(int index) }; } +internal sealed class StackFrameStateMachineMethodNameNode : StackFrameGeneratedNameNode +{ + internal readonly StackFrameGeneratedMethodNameNode EncapsulatingMethod; + internal readonly StackFrameToken GeneratedNameSeparator; + internal readonly StackFrameToken DotToken; + + internal override int ChildCount => 4; + + public StackFrameStateMachineMethodNameNode( + StackFrameGeneratedMethodNameNode encapsulatngMethod, + StackFrameToken generatedNameSeparator, + StackFrameToken dotToken, + StackFrameToken stateMachineIdentifier) + : base(stateMachineIdentifier, StackFrameKind.StateMachineMethodIdentifier) + { + Debug.Assert(generatedNameSeparator.Kind == StackFrameKind.GeneratedNameSeparatorToken); + Debug.Assert(dotToken.Kind == StackFrameKind.DotToken); + Debug.Assert(stateMachineIdentifier.Kind == StackFrameKind.IdentifierToken); + + EncapsulatingMethod = encapsulatngMethod; + GeneratedNameSeparator = generatedNameSeparator; + DotToken = dotToken; + } + + public override void Accept(IStackFrameNodeVisitor visitor) + => visitor.Visit(this); + + internal override StackFrameNodeOrToken ChildAt(int index) + => index switch + { + 0 => EncapsulatingMethod, + 1 => GeneratedNameSeparator, + 2 => DotToken, + 3 => Identifier, + _ => throw new InvalidOperationException() + }; +} + /// /// Represents an array type declaration, such as string[,][] /// diff --git a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameParser.cs b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameParser.cs index ad3f25a3fa0b..8fee9fdad343 100644 --- a/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameParser.cs +++ b/src/Features/Core/Portable/EmbeddedLanguages/StackFrame/StackFrameParser.cs @@ -248,6 +248,14 @@ private Result TryParseQualifiedName(StackFrameName /// ^----^--------------- "Local" is the name of the local function /// ^---^----------- "|0_0" is suffix information such as slot /// ^--------^- "(String s)" identifiers the method paramters + /// 3. StateMachineMethodName + /// Program.$lt;Main$gt;d__6.MoveNext() + /// ^---------------------------- Beginning of generated name + /// ^---^-------------------- Encapsulating method name + /// ^---------------- "d__6" identifies this as a state machine method + /// ^-------^--- "MoveNext" is the name of the method in the state machine + /// + /// /// /// private Result TryScanGeneratedName() @@ -284,39 +292,69 @@ private Result TryScanGeneratedName() // Check for generated name kinds we can handle // See https://github.com/dotnet/roslyn/blob/main/src/Compilers/CSharp/Portable/Symbols/Synthesized/GeneratedNameKind.cs - if (currentChar == 'g') + switch (currentChar) { - // Local function - var encapsulatingMethod = new StackFrameGeneratedMethodNameNode(lessThanToken, identifier.Value, greaterThanToken, dollarToken: null); - var (success, generatedNameSeparator) = _lexer.TryScanRequiredGeneratedNameSeparator(); - if (!success) - { - return Result.Abort; - } + case 'g': // Local function + return TryParseLocalMethodName(lessThanToken, identifier.Value, greaterThanToken); - var generatedIdentifier = _lexer.TryScanIdentifier(); - if (!generatedIdentifier.HasValue) - { - return Result.Abort; - } + case 'd': // State Machine (such as async methods) + return TryParseStateMachineMethodName(lessThanToken, identifier.Value, greaterThanToken); - if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.PipeToken, out var suffixSeparator)) - { + default: return Result.Abort; - } + } + } - (success, var suffix) = _lexer.TryScanRequiredGeneratedNameSuffix(); - if (!success) - { - return Result.Abort; - } + private Result TryParseLocalMethodName(StackFrameToken lessThanToken, StackFrameToken identifier, StackFrameToken greaterThanToken) + { + var encapsulatingMethod = new StackFrameGeneratedMethodNameNode(lessThanToken, identifier, greaterThanToken, dollarToken: null); + var (success, generatedNameSeparator) = _lexer.TryScanRequiredGeneratedNameSeparator(); + if (!success) + { + return Result.Abort; + } + + var generatedIdentifier = _lexer.TryScanIdentifier(); + if (!generatedIdentifier.HasValue) + { + return Result.Abort; + } - return new StackFrameLocalMethodNameNode(encapsulatingMethod, generatedNameSeparator, generatedIdentifier.Value, suffixSeparator, suffix); + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.PipeToken, out var suffixSeparator)) + { + return Result.Abort; } - else + + (success, var suffix) = _lexer.TryScanRequiredGeneratedNameSuffix(); + if (!success) { return Result.Abort; } + + return new StackFrameLocalMethodNameNode(encapsulatingMethod, generatedNameSeparator, generatedIdentifier.Value, suffixSeparator, suffix); + } + + private Result TryParseStateMachineMethodName(StackFrameToken lessThanToken, StackFrameToken identifier, StackFrameToken greaterThanToken) + { + var encapsulatingMethod = new StackFrameGeneratedMethodNameNode(lessThanToken, identifier, greaterThanToken, dollarToken: null); + var (success, generatedNameSeparator) = _lexer.TryScanRequiredGeneratedNameSeparator(scanNumericsAfter: true); + if (!success) + { + return Result.Abort; + } + + if (!_lexer.ScanCurrentCharAsTokenIfMatch(StackFrameKind.DotToken, out var dotToken)) + { + return Result.Abort; + } + + var generatedIdentifier = _lexer.TryScanIdentifier(); + if (!generatedIdentifier.HasValue) + { + return Result.Abort; + } + + return new StackFrameStateMachineMethodNameNode(encapsulatingMethod, generatedNameSeparator, dotToken, generatedIdentifier.Value); } /// diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/LegacySolutionEvents/UnitTestingLegacySolutionEventsListener.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/LegacySolutionEvents/UnitTestingLegacySolutionEventsListener.cs index 27045155dbab..0bf629b633eb 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/LegacySolutionEvents/UnitTestingLegacySolutionEventsListener.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/LegacySolutionEvents/UnitTestingLegacySolutionEventsListener.cs @@ -45,10 +45,10 @@ public bool ShouldReportChanges(SolutionServices services) return service.HasRegisteredAnalyzerProviders; } - public ValueTask OnWorkspaceChangedAsync(WorkspaceChangeEventArgs args, CancellationToken cancellationToken) + public ValueTask OnWorkspaceChangedAsync(WorkspaceChangeEventArgs args, bool processSourceGeneratedDocuments, CancellationToken cancellationToken) { var coordinator = GetCoordinator(args.NewSolution); - coordinator?.OnWorkspaceChanged(args); + coordinator?.OnWorkspaceChanged(args, processSourceGeneratedDocuments); return ValueTask.CompletedTask; } } diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/IUnitTestingWorkCoordinator.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/IUnitTestingWorkCoordinator.cs index 1578c5dfe5a9..c2876994ab21 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/IUnitTestingWorkCoordinator.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/IUnitTestingWorkCoordinator.cs @@ -6,5 +6,5 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.SolutionCrawler; internal interface IUnitTestingWorkCoordinator { - void OnWorkspaceChanged(WorkspaceChangeEventArgs args); + void OnWorkspaceChanged(WorkspaceChangeEventArgs args, bool processSourceGeneratedDocuments); } diff --git a/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/UnitTestingWorkCoordinator.cs b/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/UnitTestingWorkCoordinator.cs index 9350c6846ff5..f7eeed96ecae 100644 --- a/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/UnitTestingWorkCoordinator.cs +++ b/src/Features/Core/Portable/ExternalAccess/UnitTesting/SolutionCrawler/UnitTestingWorkCoordinator.cs @@ -29,7 +29,14 @@ internal sealed partial class UnitTestingWorkCoordinator : IUnitTestingWorkCoord private readonly CancellationTokenSource _shutdownNotificationSource = new(); private readonly CancellationToken _shutdownToken; - private readonly AsyncBatchingWorkQueue> _eventProcessingQueue; + + /// + /// A piece of work logged into the work coordinator queue. Includes the time the work was added, so when looking at a dump you can + /// get a sense how long things have been waiting in the queue and whether it was a slow but continuous trickle or a burst of work. + /// + private record struct TimestampedWorkItem(Func Work, DateTime TimestampAdded); + + private readonly AsyncBatchingWorkQueue _eventProcessingQueue; // points to processor task private readonly UnitTestingIncrementalAnalyzerProcessor _documentAndProjectWorkerProcessor; @@ -70,13 +77,13 @@ public UnitTestingWorkCoordinator( _semanticChangeProcessor = new UnitTestingSemanticChangeProcessor(listener, Registration, _documentAndProjectWorkerProcessor, semanticBackOffTimeSpan, projectBackOffTimeSpan, _shutdownToken); } - private async ValueTask ProcessWorkQueueAsync(ImmutableSegmentedList> list, CancellationToken cancellationToken) + private async ValueTask ProcessWorkQueueAsync(ImmutableSegmentedList list, CancellationToken cancellationToken) { - foreach (var taskCreator in list) + foreach (var workItem in list) { cancellationToken.ThrowIfCancellationRequested(); - var task = Task.Run(taskCreator, cancellationToken); + var task = Task.Run(workItem.Work, cancellationToken); _ = task.ReportNonFatalErrorAsync(); await task.NoThrowAwaitableInternal(captureContext: false); } @@ -97,7 +104,7 @@ public void AddAnalyzer(IUnitTestingIncrementalAnalyzer analyzer) public void Reanalyze(IUnitTestingIncrementalAnalyzer analyzer, UnitTestingReanalyzeScope scope) { - _eventProcessingQueue.AddWork(() => EnqueueWorkItemAsync(analyzer, scope)); + AddWork(() => EnqueueWorkItemAsync(analyzer, scope)); if (scope.HasMultipleDocuments) { @@ -109,12 +116,17 @@ public void Reanalyze(IUnitTestingIncrementalAnalyzer analyzer, UnitTestingReana } } - public void OnWorkspaceChanged(WorkspaceChangeEventArgs args) + private void AddWork(Func work) + { + _eventProcessingQueue.AddWork(new TimestampedWorkItem(work, DateTime.UtcNow)); + } + + public void OnWorkspaceChanged(WorkspaceChangeEventArgs args, bool processSourceGeneratedDocuments) { // guard us from cancellation try { - ProcessEvent(args); + ProcessEvent(args, processSourceGeneratedDocuments); } catch (OperationCanceledException oce) { @@ -144,7 +156,7 @@ public void OnWorkspaceChanged(WorkspaceChangeEventArgs args) private bool NotOurShutdownToken(OperationCanceledException oce) => oce.CancellationToken == _shutdownToken; - private void ProcessEvent(WorkspaceChangeEventArgs args) + private void ProcessEvent(WorkspaceChangeEventArgs args, bool processSourceGeneratedDocuments) { UnitTestingSolutionCrawlerLogger.LogWorkspaceEvent(_logAggregator, args.Kind); @@ -152,12 +164,12 @@ private void ProcessEvent(WorkspaceChangeEventArgs args) switch (args.Kind) { case WorkspaceChangeKind.SolutionAdded: - EnqueueFullSolutionEvent(args.NewSolution, UnitTestingInvocationReasons.DocumentAdded); + EnqueueFullSolutionEvent(args.NewSolution, UnitTestingInvocationReasons.DocumentAdded, processSourceGeneratedDocuments); break; case WorkspaceChangeKind.SolutionChanged: case WorkspaceChangeKind.SolutionReloaded: - EnqueueSolutionChangedEvent(args.OldSolution, args.NewSolution); + EnqueueSolutionChangedEvent(args.OldSolution, args.NewSolution, processSourceGeneratedDocuments); break; case WorkspaceChangeKind.SolutionCleared: @@ -167,18 +179,18 @@ private void ProcessEvent(WorkspaceChangeEventArgs args) case WorkspaceChangeKind.ProjectAdded: Contract.ThrowIfNull(args.ProjectId); - EnqueueFullProjectEvent(args.NewSolution, args.ProjectId, UnitTestingInvocationReasons.DocumentAdded); + EnqueueFullProjectEvent(args.NewSolution, args.ProjectId, UnitTestingInvocationReasons.DocumentAdded, processSourceGeneratedDocuments); break; case WorkspaceChangeKind.ProjectChanged: case WorkspaceChangeKind.ProjectReloaded: Contract.ThrowIfNull(args.ProjectId); - EnqueueProjectChangedEvent(args.OldSolution, args.NewSolution, args.ProjectId); + EnqueueProjectChangedEvent(args.OldSolution, args.NewSolution, args.ProjectId, processSourceGeneratedDocuments); break; case WorkspaceChangeKind.ProjectRemoved: Contract.ThrowIfNull(args.ProjectId); - EnqueueFullProjectEvent(args.OldSolution, args.ProjectId, UnitTestingInvocationReasons.DocumentRemoved); + EnqueueFullProjectEvent(args.OldSolution, args.ProjectId, UnitTestingInvocationReasons.DocumentRemoved, processSourceGeneratedDocuments); break; case WorkspaceChangeKind.DocumentAdded: @@ -207,7 +219,7 @@ private void ProcessEvent(WorkspaceChangeEventArgs args) case WorkspaceChangeKind.AnalyzerConfigDocumentReloaded: // If an additional file or .editorconfig has changed we need to reanalyze the entire project. Contract.ThrowIfNull(args.ProjectId); - EnqueueFullProjectEvent(args.NewSolution, args.ProjectId, UnitTestingInvocationReasons.AdditionalDocumentChanged); + EnqueueFullProjectEvent(args.NewSolution, args.ProjectId, UnitTestingInvocationReasons.AdditionalDocumentChanged, processSourceGeneratedDocuments); break; default: @@ -215,9 +227,9 @@ private void ProcessEvent(WorkspaceChangeEventArgs args) } } - private void EnqueueSolutionChangedEvent(Solution oldSolution, Solution newSolution) + private void EnqueueSolutionChangedEvent(Solution oldSolution, Solution newSolution, bool processSourceGeneratedDocuments) { - _eventProcessingQueue.AddWork( + AddWork( async () => { var solutionChanges = newSolution.GetChanges(oldSolution); @@ -225,54 +237,54 @@ private void EnqueueSolutionChangedEvent(Solution oldSolution, Solution newSolut // TODO: Async version for GetXXX methods? foreach (var addedProject in solutionChanges.GetAddedProjects()) { - await EnqueueFullProjectWorkItemAsync(addedProject, UnitTestingInvocationReasons.DocumentAdded).ConfigureAwait(false); + await EnqueueFullProjectWorkItemAsync(addedProject, UnitTestingInvocationReasons.DocumentAdded, processSourceGeneratedDocuments).ConfigureAwait(false); } foreach (var projectChanges in solutionChanges.GetProjectChanges()) { - await EnqueueWorkItemAsync(projectChanges).ConfigureAwait(continueOnCapturedContext: false); + await EnqueueWorkItemAsync(projectChanges, processSourceGeneratedDocuments).ConfigureAwait(continueOnCapturedContext: false); } foreach (var removedProject in solutionChanges.GetRemovedProjects()) { - await EnqueueFullProjectWorkItemAsync(removedProject, UnitTestingInvocationReasons.DocumentRemoved).ConfigureAwait(false); + await EnqueueFullProjectWorkItemAsync(removedProject, UnitTestingInvocationReasons.DocumentRemoved, processSourceGeneratedDocuments).ConfigureAwait(false); } }); } - private void EnqueueFullSolutionEvent(Solution solution, UnitTestingInvocationReasons invocationReasons) + private void EnqueueFullSolutionEvent(Solution solution, UnitTestingInvocationReasons invocationReasons, bool processSourceGeneratedDocuments) { - _eventProcessingQueue.AddWork( + AddWork( async () => { foreach (var projectId in solution.ProjectIds) { - await EnqueueFullProjectWorkItemAsync(solution.GetRequiredProject(projectId), invocationReasons).ConfigureAwait(false); + await EnqueueFullProjectWorkItemAsync(solution.GetRequiredProject(projectId), invocationReasons, processSourceGeneratedDocuments).ConfigureAwait(false); } }); } - private void EnqueueProjectChangedEvent(Solution oldSolution, Solution newSolution, ProjectId projectId) + private void EnqueueProjectChangedEvent(Solution oldSolution, Solution newSolution, ProjectId projectId, bool processSourceGeneratedDocuments) { - _eventProcessingQueue.AddWork( + AddWork( async () => { var oldProject = oldSolution.GetRequiredProject(projectId); var newProject = newSolution.GetRequiredProject(projectId); - await EnqueueWorkItemAsync(newProject.GetChanges(oldProject)).ConfigureAwait(false); + await EnqueueWorkItemAsync(newProject.GetChanges(oldProject), processSourceGeneratedDocuments).ConfigureAwait(false); }); } - private void EnqueueFullProjectEvent(Solution solution, ProjectId projectId, UnitTestingInvocationReasons invocationReasons) + private void EnqueueFullProjectEvent(Solution solution, ProjectId projectId, UnitTestingInvocationReasons invocationReasons, bool processSourceGeneratedDocuments) { - _eventProcessingQueue.AddWork( - () => EnqueueFullProjectWorkItemAsync(solution.GetRequiredProject(projectId), invocationReasons)); + AddWork( + () => EnqueueFullProjectWorkItemAsync(solution.GetRequiredProject(projectId), invocationReasons, processSourceGeneratedDocuments)); } private void EnqueueFullDocumentEvent(Solution solution, DocumentId documentId, UnitTestingInvocationReasons invocationReasons) { - _eventProcessingQueue.AddWork( + AddWork( () => { var project = solution.GetRequiredProject(documentId.ProjectId); @@ -283,7 +295,7 @@ private void EnqueueFullDocumentEvent(Solution solution, DocumentId documentId, private void EnqueueDocumentChangedEvent(Solution oldSolution, Solution newSolution, DocumentId documentId) { // document changed event is the special one. - _eventProcessingQueue.AddWork( + AddWork( async () => { var oldProject = oldSolution.GetRequiredProject(documentId.ProjectId); @@ -366,7 +378,7 @@ private static Document GetRequiredDocument(Project project, DocumentId document return new SyntaxPath(changedMember); } - private async Task EnqueueFullProjectWorkItemAsync(Project project, UnitTestingInvocationReasons invocationReasons) + private async Task EnqueueFullProjectWorkItemAsync(Project project, UnitTestingInvocationReasons invocationReasons, bool processSourceGeneratedDocuments) { foreach (var documentId in project.DocumentIds) await EnqueueDocumentWorkItemAsync(project, documentId, document: null, invocationReasons).ConfigureAwait(false); @@ -377,10 +389,13 @@ private async Task EnqueueFullProjectWorkItemAsync(Project project, UnitTestingI foreach (var documentId in project.AnalyzerConfigDocumentIds) await EnqueueDocumentWorkItemAsync(project, documentId, document: null, invocationReasons).ConfigureAwait(false); - // If all features are enabled for source generated documents, the solution crawler needs to - // include them in incremental analysis. - foreach (var document in await project.GetSourceGeneratedDocumentsAsync(_shutdownToken).ConfigureAwait(false)) - await EnqueueDocumentWorkItemAsync(project, document.Id, document, invocationReasons).ConfigureAwait(false); + if (processSourceGeneratedDocuments) + { + // If all features are enabled for source generated documents, the solution crawler needs to + // include them in incremental analysis. + foreach (var document in await project.GetSourceGeneratedDocumentsAsync(_shutdownToken).ConfigureAwait(false)) + await EnqueueDocumentWorkItemAsync(project, document.Id, document, invocationReasons).ConfigureAwait(false); + } } private async Task EnqueueWorkItemAsync(IUnitTestingIncrementalAnalyzer analyzer, UnitTestingReanalyzeScope scope) @@ -405,9 +420,9 @@ private async Task EnqueueWorkItemAsync( isLowPriority, analyzer, _listener.BeginAsyncOperation("WorkItem"))); } - private async Task EnqueueWorkItemAsync(ProjectChanges projectChanges) + private async Task EnqueueWorkItemAsync(ProjectChanges projectChanges, bool processSourceGeneratedDocuments) { - await EnqueueProjectConfigurationChangeWorkItemAsync(projectChanges).ConfigureAwait(false); + await EnqueueProjectConfigurationChangeWorkItemAsync(projectChanges, processSourceGeneratedDocuments).ConfigureAwait(false); foreach (var addedDocumentId in projectChanges.GetAddedDocuments()) await EnqueueDocumentWorkItemAsync(projectChanges.NewProject, addedDocumentId, document: null, UnitTestingInvocationReasons.DocumentAdded).ConfigureAwait(false); @@ -422,7 +437,7 @@ await EnqueueChangedDocumentWorkItemAsync(projectChanges.OldProject.GetRequiredD await EnqueueDocumentWorkItemAsync(projectChanges.OldProject, removedDocumentId, document: null, UnitTestingInvocationReasons.DocumentRemoved).ConfigureAwait(false); } - private async Task EnqueueProjectConfigurationChangeWorkItemAsync(ProjectChanges projectChanges) + private async Task EnqueueProjectConfigurationChangeWorkItemAsync(ProjectChanges projectChanges, bool processSourceGeneratedDocuments) { var oldProject = projectChanges.OldProject; var newProject = projectChanges.NewProject; @@ -430,29 +445,62 @@ private async Task EnqueueProjectConfigurationChangeWorkItemAsync(ProjectChanges // TODO: why solution changes return Project not ProjectId but ProjectChanges return DocumentId not Document? var projectConfigurationChange = UnitTestingInvocationReasons.Empty; - if (projectChanges.GetAddedMetadataReferences().Any() || - projectChanges.GetAddedProjectReferences().Any() || - projectChanges.GetAddedAnalyzerReferences().Any() || - projectChanges.GetRemovedMetadataReferences().Any() || - projectChanges.GetRemovedProjectReferences().Any() || - projectChanges.GetRemovedAnalyzerReferences().Any() || - !object.Equals(oldProject.CompilationOptions, newProject.CompilationOptions) || - !object.Equals(oldProject.AssemblyName, newProject.AssemblyName) || - !object.Equals(oldProject.Name, newProject.Name) || - !object.Equals(oldProject.AnalyzerOptions, newProject.AnalyzerOptions) || - !object.Equals(oldProject.HostAnalyzerOptions, newProject.HostAnalyzerOptions) || - !object.Equals(oldProject.DefaultNamespace, newProject.DefaultNamespace) || - !object.Equals(oldProject.OutputFilePath, newProject.OutputFilePath) || - !object.Equals(oldProject.OutputRefFilePath, newProject.OutputRefFilePath) || - !oldProject.CompilationOutputInfo.Equals(newProject.CompilationOutputInfo) || - oldProject.State.RunAnalyzers != newProject.State.RunAnalyzers) - { - projectConfigurationChange = projectConfigurationChange.With(UnitTestingInvocationReasons.ProjectConfigurationChanged); - } + // We will create an invocation reason for each kind of change we might detect; this makes it easy to identify in + // a memory dump why a particular project reanalysis was happening. + if (projectChanges.GetAddedMetadataReferences().Any()) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.MetadataReferences) + "Added"); + + if (projectChanges.GetAddedProjectReferences().Any()) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.ProjectReferences) + "Added"); + + if (projectChanges.GetAddedAnalyzerReferences().Any()) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.AnalyzerReferences) + "Added"); + + if (projectChanges.GetRemovedMetadataReferences().Any()) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.MetadataReferences) + "Removed"); + + if (projectChanges.GetRemovedProjectReferences().Any()) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.ProjectReferences) + "Removed"); + + if (projectChanges.GetRemovedAnalyzerReferences().Any()) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.AnalyzerReferences) + "Removed"); + + if (!object.Equals(oldProject.CompilationOptions, newProject.CompilationOptions)) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.CompilationOptions) + "Changed"); + + if (!object.Equals(oldProject.AssemblyName, newProject.AssemblyName)) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.AssemblyName) + "Changed"); + + if (!object.Equals(oldProject.Name, newProject.Name)) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.Name) + "Changed"); + + if (!object.Equals(oldProject.AnalyzerOptions, newProject.AnalyzerOptions)) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.AnalyzerOptions) + "Changed"); + + if (!object.Equals(oldProject.HostAnalyzerOptions, newProject.HostAnalyzerOptions)) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.HostAnalyzerOptions) + "Changed"); + + if (!object.Equals(oldProject.DefaultNamespace, newProject.DefaultNamespace)) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.DefaultNamespace) + "Changed"); + + if (!object.Equals(oldProject.OutputFilePath, newProject.OutputFilePath)) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.OutputFilePath) + "Changed"); + + if (!object.Equals(oldProject.OutputRefFilePath, newProject.OutputRefFilePath)) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.OutputRefFilePath) + "Changed"); + + if (!oldProject.CompilationOutputInfo.Equals(newProject.CompilationOutputInfo)) + projectConfigurationChange = projectConfigurationChange.With(nameof(oldProject.CompilationOutputInfo) + "Changed"); + + if (oldProject.State.RunAnalyzers != newProject.State.RunAnalyzers) + projectConfigurationChange = projectConfigurationChange.With(nameof(ProjectState.RunAnalyzers) + "Changed"); if (!projectConfigurationChange.IsEmpty) { - await EnqueueFullProjectWorkItemAsync(projectChanges.NewProject, projectConfigurationChange).ConfigureAwait(false); + // Also include the generic change reason which is used by other parts of the system, since nothing else looks at the specific + // reasons we created above. + projectConfigurationChange = projectConfigurationChange.With(UnitTestingPredefinedInvocationReasons.ProjectConfigurationChanged); + await EnqueueFullProjectWorkItemAsync(projectChanges.NewProject, projectConfigurationChange, processSourceGeneratedDocuments).ConfigureAwait(false); } } diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index b01dcafd5ad1..5389d5a3dfe1 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -2682,10 +2682,10 @@ Zero-width positive lookbehind assertions are typically used at the beginning of Move static members to another type... - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application @@ -2694,6 +2694,12 @@ Zero-width positive lookbehind assertions are typically used at the beginning of Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + + + {0} does not support Hot Reload. + the content of the document is stale. diff --git a/src/Features/Core/Portable/GenerateType/AbstractGenerateTypeService.State.cs b/src/Features/Core/Portable/GenerateType/AbstractGenerateTypeService.State.cs index 83030c79f685..cb95ff87bdbb 100644 --- a/src/Features/Core/Portable/GenerateType/AbstractGenerateTypeService.State.cs +++ b/src/Features/Core/Portable/GenerateType/AbstractGenerateTypeService.State.cs @@ -126,7 +126,8 @@ private async ValueTask TryInitializeAsync( } var semanticFacts = semanticDocument.Document.GetRequiredLanguageService(); - if (!semanticFacts.IsTypeContext(semanticModel, NameOrMemberAccessExpression.SpanStart, cancellationToken) && + if (!IsInsideDocumentationComment(NameOrMemberAccessExpression, syntaxFacts) && + !semanticFacts.IsTypeContext(semanticModel, NameOrMemberAccessExpression.SpanStart, cancellationToken) && !semanticFacts.IsExpressionContext(semanticModel, NameOrMemberAccessExpression.SpanStart, cancellationToken) && !semanticFacts.IsStatementContext(semanticModel, NameOrMemberAccessExpression.SpanStart, cancellationToken) && !semanticFacts.IsInsideNameOfExpression(semanticModel, NameOrMemberAccessExpression, cancellationToken) && @@ -188,6 +189,17 @@ CandidateReason.NotReferencable or return TypeToGenerateInOpt != null || NamespaceToGenerateInOpt != null; } + private static bool IsInsideDocumentationComment(SyntaxNode node, ISyntaxFactsService syntaxFacts) + { + for (var current = node; current != null; current = current.Parent) + { + if (syntaxFacts.IsDocumentationComment(current)) + return true; + } + + return false; + } + private void InferBaseType( TService service, SemanticDocument document, diff --git a/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService_Helpers.cs b/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService_Helpers.cs index 9820bb2e4cd4..3ec7a709604e 100644 --- a/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService_Helpers.cs +++ b/src/Features/Core/Portable/InheritanceMargin/AbstractInheritanceMarginService_Helpers.cs @@ -10,7 +10,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.FindSymbols.FindReferences; using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; @@ -18,6 +17,7 @@ using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.SymbolMapping; using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.TypeHierarchy; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.InheritanceMargin; @@ -56,12 +56,19 @@ private static async ValueTask> GetSymbolI } var solution = project.Solution; + var typeHierarchyService = project.GetRequiredLanguageService(); using var _ = ArrayBuilder.GetInstance(out var builder); foreach (var (symbol, lineNumber) in symbolAndLineNumbers) { if (symbol is INamedTypeSymbol namedTypeSymbol) { - await AddInheritanceMemberItemsForNamedTypeAsync(solution, namedTypeSymbol, lineNumber, builder, cancellationToken).ConfigureAwait(false); + await AddInheritanceMemberItemsForNamedTypeAsync( + solution, + typeHierarchyService, + namedTypeSymbol, + lineNumber, + builder, + cancellationToken).ConfigureAwait(false); } if (symbol is IEventSymbol or IPropertySymbol or IMethodSymbol) @@ -132,7 +139,7 @@ private async Task> GetInheritanceMarginIt var (remappedProject, symbolAndLineNumbers) = await GetMemberSymbolsAsync(document, spanToSearch, cancellationToken).ConfigureAwait(false); // if we didn't remap the symbol to another project (e.g. remapping from a metadata-as-source symbol back to - // the originating project), then we're in teh same project and we should try to get global import + // the originating project), then we're in the same project and we should try to get global import // information to display. var remapped = remappedProject != document.Project; @@ -280,13 +287,14 @@ private async Task> GetGlobalImportsItemsA private static async ValueTask AddInheritanceMemberItemsForNamedTypeAsync( Solution solution, + ITypeHierarchyService typeHierarchyService, INamedTypeSymbol memberSymbol, int lineNumber, ArrayBuilder builder, CancellationToken cancellationToken) { // Get all base types. - var allBaseSymbols = BaseTypeFinder.FindBaseTypesAndInterfaces(memberSymbol); + var allBaseSymbols = typeHierarchyService.GetBaseTypesAndInterfaces(memberSymbol); // Filter out // 1. System.Object. (otherwise margin would be shown for all classes) @@ -300,9 +308,10 @@ private static async ValueTask AddInheritanceMemberItemsForNamedTypeAsync( .WhereAsArray(symbol => !symbol.IsErrorType() && symbol.SpecialType is not (SpecialType.System_Object or SpecialType.System_ValueType or SpecialType.System_Enum)); // Get all derived types - var allDerivedSymbols = await GetDerivedTypesAndImplementationsAsync( + var allDerivedSymbols = await typeHierarchyService.GetDerivedTypesAndImplementationsAsync( solution, memberSymbol, + transitive: true, cancellationToken).ConfigureAwait(false); // Ensure the user won't be able to see symbol outside the solution for derived symbols. @@ -672,38 +681,6 @@ private static ImmutableArray GetOverriddenSymbols(ISymbol memberSymbol return builder.ToImmutableAndClear(); } - /// - /// Get the derived interfaces and derived classes for . - /// - private static async Task> GetDerivedTypesAndImplementationsAsync( - Solution solution, - INamedTypeSymbol typeSymbol, - CancellationToken cancellationToken) - { - if (typeSymbol.IsInterfaceType()) - { - var allDerivedInterfaces = await SymbolFinder.FindDerivedInterfacesArrayAsync( - typeSymbol, - solution, - transitive: true, - cancellationToken: cancellationToken).ConfigureAwait(false); - var allImplementations = await SymbolFinder.FindImplementationsArrayAsync( - typeSymbol, - solution, - transitive: true, - cancellationToken: cancellationToken).ConfigureAwait(false); - return [.. allDerivedInterfaces, .. allImplementations]; - } - else - { - return await SymbolFinder.FindDerivedClassesArrayAsync( - typeSymbol, - solution, - transitive: true, - cancellationToken: cancellationToken).ConfigureAwait(false); - } - } - /// /// Create the DefinitionItem based on the numbers of locations for . /// If there is only one location, create the DefinitionItem contains only the documentSpan or symbolKey to save memory. diff --git a/src/Features/Core/Portable/LanguageServiceIndexFormat/SymbolMoniker.cs b/src/Features/Core/Portable/LanguageServiceIndexFormat/SymbolMoniker.cs deleted file mode 100644 index f68742e4c9f9..000000000000 --- a/src/Features/Core/Portable/LanguageServiceIndexFormat/SymbolMoniker.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; - -namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat; - -internal sealed class SymbolMoniker(string scheme, string identifier) -{ - public string Scheme { get; } = scheme; - - public string Identifier { get; } = identifier; - - public static bool HasMoniker(ISymbol symbol) - { - // We don't create monikers for symbols with type substitutions - if (!symbol.OriginalDefinition.Equals(symbol)) - return false; - - // Don't create monikers for all local things that cannot escape outside of a single file: downstream consumers simply treat this as meaning a references/definition result - // doesn't need to be stitched together across files or multiple projects or repositories. - if (symbol.Kind is SymbolKind.Local or - SymbolKind.RangeVariable or - SymbolKind.Label or - SymbolKind.Alias) - { - return false; - } - - // Don't create monikers for built in-operators. We could pick some sort of moniker for these, but I doubt anybody really needs to search for all uses of - // + in the world's projects at once. - if (symbol is IMethodSymbol { MethodKind: MethodKind.BuiltinOperator }) - return false; - - // TODO: some symbols for things some things in crefs don't have a ContainingAssembly. We'll skip those for now but do - // want those to work. - if (symbol is { Kind: not SymbolKind.Namespace, ContainingAssembly: null }) - return false; - - return true; - } - - public static SymbolMoniker Create(ISymbol symbol) - { - // This uses the existing format that earlier prototypes of the Roslyn LSIF tool implemented; a different format may make more sense long term, but changing the - // moniker makes it difficult for other systems that have older LSIF indexes to the connect the two indexes together. - - if (!HasMoniker(symbol)) - throw new InvalidOperationException($"'{symbol}' does not have a moniker."); - - // Namespaces are special: they're just a name that exists in the ether between compilations - if (symbol.Kind == SymbolKind.Namespace) - { - return new SymbolMoniker(WellKnownSymbolMonikerSchemes.DotnetNamespace, symbol.ToDisplayString()); - } - - var symbolMoniker = symbol.ContainingAssembly.Name + "#"; - - if (symbol.Kind == SymbolKind.Parameter) - { - symbolMoniker += GetRequiredDocumentationCommentId(symbol.ContainingSymbol) + "#" + symbol.Name; - } - else - { - symbolMoniker += GetRequiredDocumentationCommentId(symbol); - } - - return new SymbolMoniker(WellKnownSymbolMonikerSchemes.DotnetXmlDoc, symbolMoniker); - - static string GetRequiredDocumentationCommentId(ISymbol symbol) - { - symbol = symbol.OriginalDefinition; - var documentationCommentId = symbol.GetDocumentationCommentId(); - - if (documentationCommentId == null) - { - throw new Exception($"Unable to get documentation comment ID for {symbol.ToDisplayString()}"); - } - - return documentationCommentId; - } - } -} diff --git a/src/Features/Core/Portable/LanguageServiceIndexFormat/WellKnownSymbolMonikerSchemes.cs b/src/Features/Core/Portable/LanguageServiceIndexFormat/WellKnownSymbolMonikerSchemes.cs deleted file mode 100644 index 46886be0cf4a..000000000000 --- a/src/Features/Core/Portable/LanguageServiceIndexFormat/WellKnownSymbolMonikerSchemes.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat; - -internal static class WellKnownSymbolMonikerSchemes -{ - public const string DotnetNamespace = "dotnet-namespace"; - public const string DotnetXmlDoc = "dotnet-xml-doc"; -} diff --git a/src/Features/Core/Portable/LegacySolutionEvents/ILegacySolutionEventsAggregationService.cs b/src/Features/Core/Portable/LegacySolutionEvents/ILegacySolutionEventsAggregationService.cs index 7703ebcd3c14..cf7762bfaeec 100644 --- a/src/Features/Core/Portable/LegacySolutionEvents/ILegacySolutionEventsAggregationService.cs +++ b/src/Features/Core/Portable/LegacySolutionEvents/ILegacySolutionEventsAggregationService.cs @@ -22,7 +22,7 @@ internal interface ILegacySolutionEventsAggregationService : IWorkspaceService { bool ShouldReportChanges(SolutionServices services); - ValueTask OnWorkspaceChangedAsync(WorkspaceChangeEventArgs args, CancellationToken cancellationToken); + ValueTask OnWorkspaceChangedAsync(WorkspaceChangeEventArgs args, bool processSourceGeneratedDocuments, CancellationToken cancellationToken); } [ExportWorkspaceService(typeof(ILegacySolutionEventsAggregationService)), Shared] @@ -44,9 +44,9 @@ public bool ShouldReportChanges(SolutionServices services) return false; } - public async ValueTask OnWorkspaceChangedAsync(WorkspaceChangeEventArgs args, CancellationToken cancellationToken) + public async ValueTask OnWorkspaceChangedAsync(WorkspaceChangeEventArgs args, bool processSourceGeneratedDocuments, CancellationToken cancellationToken) { foreach (var service in _eventsServices) - await service.Value.OnWorkspaceChangedAsync(args, cancellationToken).ConfigureAwait(false); + await service.Value.OnWorkspaceChangedAsync(args, processSourceGeneratedDocuments, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Features/Core/Portable/LegacySolutionEvents/ILegacySolutionEventsListener.cs b/src/Features/Core/Portable/LegacySolutionEvents/ILegacySolutionEventsListener.cs index 1bd9e8cab8e4..3b3c04baa2f4 100644 --- a/src/Features/Core/Portable/LegacySolutionEvents/ILegacySolutionEventsListener.cs +++ b/src/Features/Core/Portable/LegacySolutionEvents/ILegacySolutionEventsListener.cs @@ -16,5 +16,5 @@ namespace Microsoft.CodeAnalysis.LegacySolutionEvents; internal interface ILegacySolutionEventsListener { bool ShouldReportChanges(SolutionServices services); - ValueTask OnWorkspaceChangedAsync(WorkspaceChangeEventArgs args, CancellationToken cancellationToken); + ValueTask OnWorkspaceChangedAsync(WorkspaceChangeEventArgs args, bool processSourceGeneratedDocuments, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/LegacySolutionEvents/IRemoteLegacySolutionEventsAggregationService.cs b/src/Features/Core/Portable/LegacySolutionEvents/IRemoteLegacySolutionEventsAggregationService.cs index 8f761b556a8d..b90980d0706a 100644 --- a/src/Features/Core/Portable/LegacySolutionEvents/IRemoteLegacySolutionEventsAggregationService.cs +++ b/src/Features/Core/Portable/LegacySolutionEvents/IRemoteLegacySolutionEventsAggregationService.cs @@ -21,5 +21,6 @@ internal interface IRemoteLegacySolutionEventsAggregationService /// /// /// - ValueTask OnWorkspaceChangedAsync(Checksum oldSolutionChecksum, Checksum newSolutionChecksum, WorkspaceChangeKind kind, ProjectId? projectId, DocumentId? documentId, CancellationToken cancellationToken); + /// Whether a change to a project should result in source-generated documents being refreshed. + ValueTask OnWorkspaceChangedAsync(Checksum oldSolutionChecksum, Checksum newSolutionChecksum, WorkspaceChangeKind kind, ProjectId? projectId, DocumentId? documentId, bool processSourceGeneratedDocuments, CancellationToken cancellationToken); } diff --git a/src/Features/Core/Portable/MetadataAsSource/DecompilationMetadataAsSourceFileProvider.cs b/src/Features/Core/Portable/MetadataAsSource/DecompilationMetadataAsSourceFileProvider.cs index 53979b0fe209..cf37678cc108 100644 --- a/src/Features/Core/Portable/MetadataAsSource/DecompilationMetadataAsSourceFileProvider.cs +++ b/src/Features/Core/Portable/MetadataAsSource/DecompilationMetadataAsSourceFileProvider.cs @@ -242,22 +242,27 @@ private void MutateWorkspace(DocumentId temporaryDocumentId, MetadataAsSourceGen var isReferenceAssembly = MetadataAsSourceHelpers.IsReferenceAssembly(containingAssembly); - if (assemblyLocation is not null && - isReferenceAssembly && - !_implementationAssemblyLookupService.TryFindImplementationAssemblyPath(assemblyLocation, out assemblyLocation)) + if (assemblyLocation is not null && isReferenceAssembly) { - try + if (_implementationAssemblyLookupService.TryFindImplementationAssemblyPath(assemblyLocation, out assemblyLocation)) { - var fullAssemblyName = containingAssembly.Identity.GetDisplayName(); - GlobalAssemblyCache.Instance.ResolvePartialName(fullAssemblyName, out assemblyLocation, preferredCulture: CultureInfo.CurrentCulture); - isReferenceAssembly = assemblyLocation is null; + isReferenceAssembly = false; } - catch (IOException) - { - // If we get an IO exception we can safely ignore it, and the system will show the metadata view of the reference assembly. - } - catch (Exception e) when (FatalError.ReportAndCatch(e, ErrorSeverity.Diagnostic)) + else { + try + { + var fullAssemblyName = containingAssembly.Identity.GetDisplayName(); + GlobalAssemblyCache.Instance.ResolvePartialName(fullAssemblyName, out assemblyLocation, preferredCulture: CultureInfo.CurrentCulture); + isReferenceAssembly = assemblyLocation is null; + } + catch (IOException) + { + // If we get an IO exception we can safely ignore it, and the system will show the metadata view of the reference assembly. + } + catch (Exception e) when (FatalError.ReportAndCatch(e, ErrorSeverity.Diagnostic)) + { + } } } diff --git a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj index 4676163513d5..073e817e07f5 100644 --- a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj +++ b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj @@ -65,8 +65,6 @@ - - diff --git a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs index d5b37c9fa304..49f811d2eb67 100644 --- a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs +++ b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -22,6 +22,7 @@ namespace Microsoft.CodeAnalysis.NavigateTo; using CachedIndexMap = ConcurrentDictionary<(IChecksummedPersistentStorageService service, DocumentKey documentKey, StringTable stringTable), AsyncLazy>; +using CachedFilterIndexMap = ConcurrentDictionary<(IChecksummedPersistentStorageService service, DocumentKey documentKey, StringTable stringTable), AsyncLazy>; internal abstract partial class AbstractNavigateToSearchService { @@ -32,6 +33,12 @@ internal abstract partial class AbstractNavigateToSearchService /// private static CachedIndexMap? s_cachedIndexMap = []; + /// + /// Cached map from document key to the (potentially stale) lightweight filter index. Loaded first + /// to quickly reject documents before loading the much larger . + /// + private static CachedFilterIndexMap? s_cachedFilterIndexMap = []; + /// /// String table we use to dedupe common values while deserializing s. Once the /// full solution is available, this will be dropped (set to ) to release all cached data. @@ -43,16 +50,19 @@ private static void ClearCachedData() // Volatiles are technically not necessary due to automatic fencing of reference-type writes. However, // i prefer the explicitness here as we are reading and writing these fields from different threads. Volatile.Write(ref s_cachedIndexMap, null); + Volatile.Write(ref s_cachedFilterIndexMap, null); Volatile.Write(ref s_stringTable, null); } private static bool ShouldSearchCachedDocuments( [NotNullWhen(true)] out CachedIndexMap? cachedIndexMap, + [NotNullWhen(true)] out CachedFilterIndexMap? cachedFilterIndexMap, [NotNullWhen(true)] out StringTable? stringTable) { cachedIndexMap = Volatile.Read(ref s_cachedIndexMap); + cachedFilterIndexMap = Volatile.Read(ref s_cachedFilterIndexMap); stringTable = Volatile.Read(ref s_stringTable); - return cachedIndexMap != null && stringTable != null; + return cachedIndexMap != null && cachedFilterIndexMap != null && stringTable != null; } public async Task SearchCachedDocumentsAsync( @@ -107,10 +117,12 @@ public static async Task SearchCachedDocumentsInCurrentProcessAsync( CancellationToken cancellationToken) { // Quick abort if OOP is now fully loaded. - if (!ShouldSearchCachedDocuments(out _, out _)) + if (!ShouldSearchCachedDocuments(out _, out _, out _)) + return; + + if (ProcessSearchPattern(searchPattern) is not { } patternInfo) return; - var (patternName, patternContainer) = PatternMatcher.GetNameAndContainer(searchPattern); var declaredSymbolInfoKindsSet = new DeclaredSymbolInfoKindSet(kinds); // Process the documents by project group. That way, when each project is done, we can @@ -143,13 +155,21 @@ await Parallel.ForEachAsync( cancellationToken, async (documentKey, cancellationToken) => { - var index = await GetIndexAsync(storageService, documentKey, cancellationToken).ConfigureAwait(false); + var filterIndex = await GetFilterIndexAsync(storageService, documentKey, cancellationToken).ConfigureAwait(false); + if (filterIndex == null) + return; + + if (!CouldContainMatch(filterIndex, patternInfo, out var nameMatchKinds)) + return; + + // The filter passed — now load the full index with all declared symbols. + var index = await GetFullIndexAsync(storageService, documentKey, cancellationToken).ConfigureAwait(false); if (index == null) return; ProcessIndex( - documentKey, document: null, patternName, patternContainer, declaredSymbolInfoKindsSet, - index, linkedIndices: null, onItemFound, cancellationToken); + documentKey, document: null, patternInfo, declaredSymbolInfoKindsSet, + nameMatchKinds, index, linkedIndices: null, onItemFound, cancellationToken); }).ConfigureAwait(false); // done with project. Let the host know. @@ -157,7 +177,26 @@ await Parallel.ForEachAsync( } } - private static Task GetIndexAsync( + private static async Task GetFilterIndexAsync( + IChecksummedPersistentStorageService storageService, + DocumentKey documentKey, + CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + return null; + + if (!ShouldSearchCachedDocuments(out _, out var cachedFilterIndexMap, out var stringTable)) + return null; + + var asyncLazy = cachedFilterIndexMap.GetOrAdd( + (storageService, documentKey, stringTable), + static t => AsyncLazy.Create(static (t, c) => + NavigateToSearchIndex.LoadAsync(t.service, t.documentKey, checksum: null, t.stringTable, c), + arg: t)); + return await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false); + } + + private static Task GetFullIndexAsync( IChecksummedPersistentStorageService storageService, DocumentKey documentKey, CancellationToken cancellationToken) @@ -167,7 +206,7 @@ await Parallel.ForEachAsync( // Retrieve the string table we use to dedupe strings. If we can't get it, that means the solution has // fully loaded and we've switched over to normal navto lookup. - if (!ShouldSearchCachedDocuments(out var cachedIndexMap, out var stringTable)) + if (!ShouldSearchCachedDocuments(out var cachedIndexMap, out _, out var stringTable)) return SpecializedTasks.Null(); // Add the async lazy to compute the index for this document. Or, return the existing cached one if already diff --git a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.GeneratedDocumentSearch.cs b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.GeneratedDocumentSearch.cs index f4ba6eff1b81..46ce0dff171b 100644 --- a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.GeneratedDocumentSearch.cs +++ b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.GeneratedDocumentSearch.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -65,7 +65,9 @@ public static async Task SearchGeneratedDocumentsInCurrentProcessAsync( Func onProjectCompleted, CancellationToken cancellationToken) { - var (patternName, patternContainerOpt) = PatternMatcher.GetNameAndContainer(pattern); + if (ProcessSearchPattern(pattern) is not { } patternInfo) + return; + var declaredSymbolInfoKindsSet = new DeclaredSymbolInfoKindSet(kinds); await ProducerConsumer.RunParallelAsync( @@ -82,7 +84,7 @@ await Parallel.ForEachAsync( sourceGeneratedDocs, cancellationToken, (document, cancellationToken) => SearchSingleDocumentAsync( - document, patternName, patternContainerOpt, declaredSymbolInfoKindsSet, onItemFound, cancellationToken)).ConfigureAwait(false); + document, patternInfo, declaredSymbolInfoKindsSet, onItemFound, cancellationToken)).ConfigureAwait(false); await onProjectCompleted().ConfigureAwait(false); } diff --git a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.InProcess.cs b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.InProcess.cs index 682193e773c5..39de1e943c77 100644 --- a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.InProcess.cs +++ b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.InProcess.cs @@ -9,7 +9,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.EmbeddedLanguages.RegularExpressions; +using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.PatternMatching; using Microsoft.CodeAnalysis.PooledObjects; @@ -35,13 +38,58 @@ internal abstract partial class AbstractNavigateToSearchService (PatternMatchKind.CamelCaseSubstring, NavigateToMatchKind.CamelCaseSubstring), (PatternMatchKind.CamelCaseNonContiguousSubstring, NavigateToMatchKind.CamelCaseNonContiguousSubstring), (PatternMatchKind.Fuzzy, NavigateToMatchKind.Fuzzy), + + // LowercaseSubstring is the weakest non-fuzzy PatternMatchKind (an all-lowercase pattern found + // inside a candidate at a non-word-boundary, e.g. "line" in "Readline"). NavigateToMatchKind has + // no dedicated bucket for it, so we map it to Fuzzy as the closest available quality tier. (PatternMatchKind.LowercaseSubstring, NavigateToMatchKind.Fuzzy), ]; + /// + /// Determines the name and container from a search pattern, using regex-aware splitting when + /// the pattern contains regex metacharacters. Also compiles a for + /// pre-filtering when the pattern is a regex. Returns if the pattern + /// is detected as regex but is invalid or has no extractable literals for pre-filtering + /// (e.g. .*), since we refuse to run a regex search that can't be narrowed down. + /// + private static SearchPatternInfo? ProcessSearchPattern(string searchPattern) + { + if (RegexPatternDetector.IsRegexPattern(searchPattern)) + { + var sequence = VirtualCharSequence.Create(0, searchPattern); + var tree = RegexParser.TryParse(sequence, RegexOptions.None); + if (tree is not { Diagnostics: [] }) + return null; + + var (container, name) = RegexPatternDetector.SplitOnContainerDot(searchPattern, tree); + + // Reuse the already-parsed tree when the full pattern is the name (no split). + // When a split occurred, the name is a substring that needs its own parse. + var regexQuery = container is null + ? RegexQueryCompiler.Compile(tree) + : RegexQueryCompiler.Compile(name); + + // Compile returns null if the regex is invalid or has no extractable literals. + // We only run regex search when the compiled query tree can genuinely filter + // documents. After optimization, None never appears as a child of Any (it poisons + // the disjunction) or All (it's pruned as vacuously true), and the compiler only + // emits Literal nodes for strings of 2+ characters (which produce real bigram + // checks). So a non-null result guarantees every Literal in the tree is reachable + // and can reject documents — the pre-filter will never degenerate to "accept + // everything." + if (regexQuery is null) + return null; + + return new SearchPatternInfo(name, container, regexQuery); + } + + var (patternName, containerOpt) = PatternMatcher.GetNameAndContainer(searchPattern); + return new SearchPatternInfo(patternName, containerOpt, RegexQuery: null); + } + private static async ValueTask SearchSingleDocumentAsync( Document document, - string patternName, - string? patternContainer, + SearchPatternInfo patternInfo, DeclaredSymbolInfoKindSet kinds, Action onItemFound, CancellationToken cancellationToken) @@ -49,8 +97,13 @@ private static async ValueTask SearchSingleDocumentAsync( if (cancellationToken.IsCancellationRequested) return; - // Get the index for the file we're searching, as well as for its linked siblings. We'll use the latter to add - // the information to a symbol about all the project TFMs is can be found in. + // First, load the lightweight filter index to check if this document could possibly match. + // This avoids loading the much larger TopLevelSyntaxTreeIndex for non-matching documents. + var filterIndex = await NavigateToSearchIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); + if (!CouldContainMatch(filterIndex, patternInfo, out var matchKinds)) + return; + + // The filter passed — now load the full index with all declared symbols. var index = await TopLevelSyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); using var _ = ArrayBuilder<(TopLevelSyntaxTreeIndex, ProjectId)>.GetInstance(out var linkedIndices); @@ -62,16 +115,29 @@ private static async ValueTask SearchSingleDocumentAsync( } ProcessIndex( - DocumentKey.ToDocumentKey(document), document, patternName, patternContainer, kinds, - index, linkedIndices, onItemFound, cancellationToken); + DocumentKey.ToDocumentKey(document), document, patternInfo, kinds, + matchKinds, index, linkedIndices, onItemFound, cancellationToken); + } + + private static bool CouldContainMatch( + NavigateToSearchIndex filterIndex, + SearchPatternInfo patternInfo, + out PatternMatcherKind matchKinds) + { + if (patternInfo.RegexQuery is { } regexQuery) + matchKinds = filterIndex.RegexQueryCheckPasses(regexQuery) ? PatternMatcherKind.Standard : PatternMatcherKind.None; + else + matchKinds = filterIndex.CouldContainNavigateToMatch(patternInfo.Name, patternInfo.Container); + + return matchKinds != PatternMatcherKind.None; } private static void ProcessIndex( DocumentKey documentKey, Document? document, - string patternName, - string? patternContainer, + SearchPatternInfo patternInfo, DeclaredSymbolInfoKindSet kinds, + PatternMatcherKind matchKinds, TopLevelSyntaxTreeIndex index, ArrayBuilder<(TopLevelSyntaxTreeIndex, ProjectId)>? linkedIndices, Action onItemFound, @@ -80,12 +146,13 @@ private static void ProcessIndex( if (cancellationToken.IsCancellationRequested) return; - var containerMatcher = patternContainer != null - ? PatternMatcher.CreateDotSeparatedContainerMatcher(patternContainer, includeMatchedSpans: true) - : null; + using var nameMatcher = PatternMatcher.CreateNameMatcher( + patternInfo.Name, patternInfo.IsRegex, includeMatchedSpans: true, matchKinds); + if (nameMatcher is null) + return; - using var nameMatcher = PatternMatcher.CreatePatternMatcher(patternName, includeMatchedSpans: true, allowFuzzyMatching: true); - using var _1 = containerMatcher; + using var containerMatcher = PatternMatcher.CreateContainerMatcher( + patternInfo.Container, patternInfo.IsRegex, includeMatchedSpans: true); foreach (var declaredSymbolInfo in index.DeclaredSymbolInfos) { @@ -215,6 +282,9 @@ private static string GetItemKind(DeclaredSymbolInfo declaredSymbolInfo) case DeclaredSymbolInfoKind.Property: return NavigateToItemKind.Property; case DeclaredSymbolInfoKind.Struct: + // Tracked by https://github.com/dotnet/roslyn/issues/82607 + // Consider having a separate NavigateToItemKind category for unions + case DeclaredSymbolInfoKind.Union: return NavigateToItemKind.Structure; case DeclaredSymbolInfoKind.Operator: return NavigateToItemKind.OtherSymbol; @@ -307,6 +377,7 @@ public DeclaredSymbolInfoKindSet(IEnumerable navigateToItemKinds) case NavigateToItemKind.Structure: lookupTable[(int)DeclaredSymbolInfoKind.Struct] = true; lookupTable[(int)DeclaredSymbolInfoKind.RecordStruct] = true; + lookupTable[(int)DeclaredSymbolInfoKind.Union] = true; break; default: diff --git a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.NormalSearch.cs b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.NormalSearch.cs index 0409588b5b61..450b6f6be291 100644 --- a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.NormalSearch.cs +++ b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.NormalSearch.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -57,7 +57,9 @@ public static async Task SearchDocumentAndRelatedDocumentsInCurrentProcessAsync( Func, VoidResult, CancellationToken, Task> onItemsFound, CancellationToken cancellationToken) { - var (patternName, patternContainerOpt) = PatternMatcher.GetNameAndContainer(searchPattern); + if (ProcessSearchPattern(searchPattern) is not { } patternInfo) + return; + var declaredSymbolInfoKindsSet = new DeclaredSymbolInfoKindSet(kinds); // In parallel, search both the document requested, and any relevant 'related documents' we find for it. For the @@ -73,9 +75,9 @@ Task SearchDocumentsInCurrentProcessAsync(ImmutableArray<(Document document, Nor documentAndSpans, produceItems: static async (documentAndSpan, onItemFound, args, cancellationToken) => { - var (patternName, patternContainerOpt, declaredSymbolInfoKindsSet, onItemsFound) = args; + var (patternInfo, declaredSymbolInfoKindsSet, onItemsFound) = args; await SearchSingleDocumentAsync( - documentAndSpan.document, patternName, patternContainerOpt, declaredSymbolInfoKindsSet, + documentAndSpan.document, patternInfo, declaredSymbolInfoKindsSet, item => { // Ensure that the results found while searching the single document intersect the desired @@ -88,7 +90,7 @@ await SearchSingleDocumentAsync( cancellationToken).ConfigureAwait(false); }, consumeItems: static (values, args, cancellationToken) => args.onItemsFound(values, default, cancellationToken), - args: (patternName, patternContainerOpt, declaredSymbolInfoKindsSet, onItemsFound), + args: (patternInfo, declaredSymbolInfoKindsSet, onItemsFound), cancellationToken); async Task SearchRelatedDocumentsInCurrentProcessAsync() @@ -196,7 +198,9 @@ public static async Task SearchProjectsInCurrentProcessAsync( // of potentially stale indices. ClearCachedData(); - var (patternName, patternContainerOpt) = PatternMatcher.GetNameAndContainer(searchPattern); + if (ProcessSearchPattern(searchPattern) is not { } patternInfo) + return; + var declaredSymbolInfoKindsSet = new DeclaredSymbolInfoKindSet(kinds); using var _ = GetPooledHashSet(priorityDocuments.Select(d => d.Project), out var highPriProjects); @@ -220,7 +224,7 @@ await Parallel.ForEachAsync( Prioritize(project.Documents, highPriDocs.Contains), cancellationToken, (document, cancellationToken) => SearchSingleDocumentAsync( - document, patternName, patternContainerOpt, declaredSymbolInfoKindsSet, onItemFound, cancellationToken)).ConfigureAwait(false); + document, patternInfo, declaredSymbolInfoKindsSet, onItemFound, cancellationToken)).ConfigureAwait(false); await onProjectCompleted().ConfigureAwait(false); } diff --git a/src/Features/Core/Portable/NavigateTo/RegexPatternDetector.cs b/src/Features/Core/Portable/NavigateTo/RegexPatternDetector.cs new file mode 100644 index 000000000000..38085f51ccb5 --- /dev/null +++ b/src/Features/Core/Portable/NavigateTo/RegexPatternDetector.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.EmbeddedLanguages.RegularExpressions; + +namespace Microsoft.CodeAnalysis.NavigateTo; + +/// +/// Detects whether a NavigateTo search pattern contains regex syntax, and performs +/// regex-aware splitting of the pattern into container and name portions. +/// +internal static class RegexPatternDetector +{ + /// + /// Returns if contains any regex + /// metacharacter that distinguishes it from a plain text search. The set includes: + /// | ( ) [ ] { } + ? * \ ^ $. + /// + /// A bare . alone does NOT trigger regex mode — it is treated as a container/name + /// separator by the existing NavigateTo logic. However, .*, .+, \.}, + /// etc. do trigger it because *, +, \ are in the metacharacter set. + /// + public static bool IsRegexPattern(string pattern) + { + foreach (var ch in pattern) + { + switch (ch) + { + case '|': + case '(': + case ')': + case '[': + case ']': + case '{': + case '}': + case '+': + case '?': + case '*': + case '\\': + case '^': + case '$': + return true; + } + } + + return false; + } + + /// + /// Splits a regex pattern into container and name portions by finding the last unquantified + /// (bare .) in the top-level sequence of the parsed + /// regex AST. + /// + /// A bare dot is structurally distinct from \. (an escape node) and .* / .+ + /// / .? (a wildcard wrapped in a quantifier). Using the parsed AST avoids ad-hoc + /// lexical scanning and handles all edge cases (escapes, character classes, nested groups) + /// correctly. + /// + /// We split on the last bare wildcard, consistent with how the existing + /// PatternMatcher.GetNameAndContainer uses LastIndexOf('.'). This keeps + /// the name portion minimal (matching DeclaredSymbolInfo.Name) and the container + /// portion maximal (matching the fully-qualified container). + /// + /// + /// A tuple of (container substring, name substring). If no split point is found, container + /// is and name is the full pattern. + /// + public static (string? container, string name) SplitOnContainerDot(string pattern, RegexTree tree) + { + // The Roslyn regex parser wraps the root in an alternation node even when there's no `|`. + // We only split at the top-level sequence — a dot inside an alternation branch (e.g. + // `Goo.Bar|Baz.Quux`) is ambiguous and doesn't make sense as a single container/name split. + var rootExpr = tree.Root.Expression; + if (rootExpr is not RegexAlternationNode { SequenceList: [var topSequence] }) + return (null, pattern); + + // Walk right-to-left to find the last bare dot. The direction is mostly arbitrary, but it + // mirrors how qualified names work: for `A.B.C`, the last dot separates the container `A.B` + // from the name `C`, which is consistent with how `PatternMatcher.GetNameAndContainer` uses + // `LastIndexOf('.')`. + // + // A RegexWildcardNode that appears directly as a child of the top-level sequence (not + // wrapped in a quantifier) represents a bare `.`. If it were quantified (e.g. `.*`), the + // parser would wrap it in a quantifier node and it wouldn't appear directly as a + // RegexWildcardNode child. + for (var i = topSequence.Children.Length - 1; i >= 0; i--) + { + if (topSequence.Children[i] is RegexWildcardNode wildcard) + { + var dotSpan = wildcard.DotToken.VirtualChars[0].Span; + var containerEnd = dotSpan.Start; + var nameStart = dotSpan.End; + + // Skip dots at the very start or end — they can't form a valid container/name pair. + if (containerEnd == 0 || nameStart >= pattern.Length) + continue; + + return (pattern[..containerEnd], pattern[nameStart..]); + } + } + + return (null, pattern); + } +} diff --git a/src/Features/Core/Portable/NavigateTo/RegexQueryCompiler.cs b/src/Features/Core/Portable/NavigateTo/RegexQueryCompiler.cs new file mode 100644 index 000000000000..3dcfedfaac7f --- /dev/null +++ b/src/Features/Core/Portable/NavigateTo/RegexQueryCompiler.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis.EmbeddedLanguages.Common; +using Microsoft.CodeAnalysis.EmbeddedLanguages.RegularExpressions; +using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; +using Microsoft.CodeAnalysis.PatternMatching; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.NavigateTo; + +/// +/// Compiles a regex pattern string into a tree that can be evaluated +/// against a document's indexed bigrams/trigrams for fast pre-filtering. +/// +/// The key insight is that certain regex constructs guarantee literal text in any match. For +/// example, (Read|Write)Line guarantees that "Line" appears in every match. By extracting +/// these guarantees into a boolean query tree, we can reject documents that lack the required +/// bigrams/trigrams without ever running the expensive full regex match. +/// +/// Constructs that cannot guarantee literal text (wildcards, character classes, zero-count +/// quantifiers) produce , which means "I can't tell — don't reject +/// on my account." If the entire compiled tree is (e.g. for .*), +/// is and the caller skips pre-filtering +/// entirely — every document is a candidate and must be checked with the full regex. +/// +internal static class RegexQueryCompiler +{ + /// + /// Parses as a .NET regex and compiles it into an optimized + /// tree. Returns if the pattern is not + /// a valid regex. + /// + public static RegexQuery? Compile(string pattern) + { + var sequence = VirtualCharSequence.Create(0, pattern); + var tree = RegexParser.TryParse(sequence, RegexOptions.None); + if (tree is not { Diagnostics: [] }) + return null; + + return Compile(tree); + } + + /// + /// Compiles an already-parsed regex AST into an optimized tree. + /// Walks the AST to extract literal requirements, then simplifies the resulting boolean tree + /// (flatten nested All/Any, prune None from All, collapse single-child wrappers). Returns + /// if the optimized tree has no extractable literals, since such a + /// query cannot filter any documents and would degenerate to "accept everything." + /// + /// When non-null, the returned tree is guaranteed to contain only , + /// , and nodes — no + /// nodes survive optimization (see + /// ). Callers can rely on this when traversing the tree. + /// + public static RegexQuery? Compile(RegexTree tree) + { + var raw = CompileNode(tree.Root.Expression); + var optimized = RegexQuery.Optimize(raw); + return optimized.HasLiterals ? optimized : null; + } + + private static RegexQuery CompileNode(RegexNode node) + { + return node switch + { + RegexAlternationNode alternation => CompileAlternation(alternation), + RegexSequenceNode seq => CompileSequence(seq), + RegexTextNode text => CompileText(text), + + // A bare wildcard (`.`) matches any character — no literal requirement can be extracted. + RegexWildcardNode => RegexQuery.None.Instance, + + // Grouping nodes are transparent for pre-filtering purposes — only the inner expression + // matters. Capture semantics don't affect which literals must appear in a match. + RegexSimpleGroupingNode group => CompileNode(group.Expression), + RegexNonCapturingGroupingNode group => CompileNode(group.Expression), + RegexCaptureGroupingNode group => CompileNode(group.Expression), + RegexNestedOptionsGroupingNode group => CompileNode(group.Expression), + RegexAtomicGroupingNode group => CompileNode(group.Expression), + + // `+` requires at least one occurrence, so the inner expression's literals must appear. + // `*` and `?` allow zero occurrences, so we can't require anything from them. + RegexOneOrMoreQuantifierNode quantifier => CompileNode(quantifier.Expression), + RegexZeroOrMoreQuantifierNode => RegexQuery.None.Instance, + RegexZeroOrOneQuantifierNode => RegexQuery.None.Instance, + + // Lazy quantifiers (e.g. `X+?`) don't change the minimum occurrence count, + // so we delegate to the underlying quantifier node for the same reasoning. + RegexLazyQuantifierNode lazy => CompileNode(lazy.Quantifier), + + // Numeric quantifiers: `{n}`, `{n,}`, `{n,m}`. If the minimum count (first number) + // is >= 1, the inner expression must appear at least once. + RegexExactNumericQuantifierNode exact => CompileNumericQuantifier(exact.Expression, exact.FirstNumberToken), + RegexOpenNumericRangeQuantifierNode open => CompileNumericQuantifier(open.Expression, open.FirstNumberToken), + RegexClosedNumericRangeQuantifierNode closed => CompileNumericQuantifier(closed.Expression, closed.FirstNumberToken), + + // Simple escape (e.g. `\.`, `\[`, `\n`): the escaped character is a required literal. + RegexSimpleEscapeNode escape => CompileSimpleEscape(escape), + + // Everything else (anchors `^`/`$`, character classes `[a-z]`, `\d`, `\w`, backreferences, + // lookahead/lookbehind, etc.) cannot contribute literal text we can require. Return None + // so the pre-filter doesn't over-reject. + _ => RegexQuery.None.Instance, + }; + } + + private static RegexQuery CompileAlternation(RegexAlternationNode alternation) + { + if (alternation.SequenceList.Length == 1) + return CompileNode(alternation.SequenceList[0]); + + var children = new FixedSizeArrayBuilder(alternation.SequenceList.Length); + for (var i = 0; i < alternation.SequenceList.Length; i++) + children.Add(CompileNode(alternation.SequenceList[i])); + + return new RegexQuery.Any(children.MoveToImmutable()); + } + + private static RegexQuery CompileSequence(RegexSequenceNode sequence) + { + if (sequence.Children.Length == 1) + return CompileNode(sequence.Children[0]); + + var children = new FixedSizeArrayBuilder(sequence.Children.Length); + foreach (var child in sequence.Children) + children.Add(CompileNode(child)); + + return new RegexQuery.All(children.MoveToImmutable()); + } + + private static RegexQuery CompileText(RegexTextNode text) + { + // Strip whitespace from text nodes because symbol names never contain whitespace. + // This allows users to write readable patterns like `( Read | Write ) Line` — + // the whitespace around alternation branches is purely for readability. + // + // Also lowercase the literal at compile time so that RegexLiteralCheckPasses doesn't + // need to allocate and lowercase on every call — the bigram index is already lowercased. + var chars = text.TextToken.VirtualChars; + using var _ = PooledStringBuilder.GetInstance(out var builder); + foreach (var ch in chars) + { + if (!char.IsWhiteSpace(ch.Value)) + builder.Append(char.ToLowerInvariant(ch.Value)); + } + + // Single characters can't form a bigram, so they provide no pre-filter value. + if (builder.Length < 2) + return RegexQuery.None.Instance; + + return new RegexQuery.Literal(builder.ToString()); + } + + private static RegexQuery CompileSimpleEscape(RegexSimpleEscapeNode _) + { + // A single escaped character (e.g. `\.`, `\[`) is just one character — it can't form + // a bigram and provides no pre-filter value, so treat it as None. + return RegexQuery.None.Instance; + } + + private static RegexQuery CompileNumericQuantifier(RegexExpressionNode expression, EmbeddedSyntaxToken firstNumberToken) + { + // Only require the inner expression's literals when the minimum repetition count is >= 1. + // `{0}` or `{0,5}` allow zero matches, meaning the inner text need not appear at all. + var numberText = firstNumberToken.VirtualChars.CreateString(); + if (int.TryParse(numberText, out var minCount) && minCount >= 1) + return CompileNode(expression); + + return RegexQuery.None.Instance; + } +} diff --git a/src/Features/Core/Portable/NavigateTo/RoslynNavigateToItem.cs b/src/Features/Core/Portable/NavigateTo/RoslynNavigateToItem.cs index b55f11f73a38..5cdd2f04df1a 100644 --- a/src/Features/Core/Portable/NavigateTo/RoslynNavigateToItem.cs +++ b/src/Features/Core/Portable/NavigateTo/RoslynNavigateToItem.cs @@ -188,6 +188,7 @@ private static bool IsNamedType(in RoslynNavigateToItem item) case DeclaredSymbolInfoKind.Module: case DeclaredSymbolInfoKind.Struct: case DeclaredSymbolInfoKind.RecordStruct: + case DeclaredSymbolInfoKind.Union: return true; default: return false; diff --git a/src/Features/Core/Portable/NavigateTo/SearchPatternInfo.cs b/src/Features/Core/Portable/NavigateTo/SearchPatternInfo.cs new file mode 100644 index 000000000000..ad46cb08c67e --- /dev/null +++ b/src/Features/Core/Portable/NavigateTo/SearchPatternInfo.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.PatternMatching; + +namespace Microsoft.CodeAnalysis.NavigateTo; + +/// +/// Holds the result of parsing and analyzing a NavigateTo search pattern. Bundles the +/// split name/container and pre-compiled regex query so callers can pass a single value +/// through the search pipeline. A non-null indicates regex mode. +/// +internal readonly record struct SearchPatternInfo( + string Name, + string? Container, + RegexQuery? RegexQuery) +{ + public bool IsRegex => RegexQuery is not null; +} diff --git a/src/Features/Core/Portable/SolutionCrawler/SolutionCrawlerService.cs b/src/Features/Core/Portable/SolutionCrawler/SolutionCrawlerService.cs index 4dfc88ec81f9..f79fe4f0e184 100644 --- a/src/Features/Core/Portable/SolutionCrawler/SolutionCrawlerService.cs +++ b/src/Features/Core/Portable/SolutionCrawler/SolutionCrawlerService.cs @@ -9,4 +9,5 @@ namespace Microsoft.CodeAnalysis.SolutionCrawler; internal sealed partial class SolutionCrawlerRegistrationService { internal static readonly Option2 EnableSolutionCrawler = new("dotnet_enable_solution_crawler", defaultValue: true); + internal static readonly Option2 ProcessRoslynSourceGeneratedFiles = new Option2("dotnet_process_roslyn_source_generated_files_in_solution_crawler", defaultValue: true); } diff --git a/src/Features/Core/Portable/SplitOrMergeIfStatements/AbstractSplitIfStatementCodeRefactoringProvider.cs b/src/Features/Core/Portable/SplitOrMergeIfStatements/AbstractSplitIfStatementCodeRefactoringProvider.cs index fdcf3a12e517..aac2278f16f6 100644 --- a/src/Features/Core/Portable/SplitOrMergeIfStatements/AbstractSplitIfStatementCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/SplitOrMergeIfStatements/AbstractSplitIfStatementCodeRefactoringProvider.cs @@ -5,7 +5,6 @@ #nullable disable using System; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; @@ -65,14 +64,19 @@ private async Task RefactorAsync(Document document, TextSpan tokenSpan var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var token = root.FindToken(tokenSpan.Start); - var ifOrElseIf = root.FindNode(ifOrElseIfSpan); - - Debug.Assert(ifGenerator.IsIfOrElseIf(ifOrElseIf)); + var ifOrElseIf = FindIfOrElseIf(ifOrElseIfSpan); var (left, right) = SplitBinaryExpressionChain(token, ifGenerator.GetCondition(ifOrElseIf), syntaxFacts); var newRoot = await GetChangedRootAsync(document, root, ifOrElseIf, left, right, cancellationToken).ConfigureAwait(false); return document.WithSyntaxRoot(newRoot); + + SyntaxNode FindIfOrElseIf(TextSpan span) + { + var innerMatch = root.FindNode(span, getInnermostNodeForTie: true); + return innerMatch?.FirstAncestorOrSelf( + node => ifGenerator.IsIfOrElseIf(node) && node.Span == span); + } } private static bool IsPartOfBinaryExpressionChain(SyntaxToken token, int syntaxKind, out SyntaxNode rootExpression) diff --git a/src/Features/Core/Portable/SymbolSearch/Windows/IDatabaseFactoryService.cs b/src/Features/Core/Portable/SymbolSearch/Windows/IDatabaseFactoryService.cs index 6ab4a9fe280f..3568445da0cc 100644 --- a/src/Features/Core/Portable/SymbolSearch/Windows/IDatabaseFactoryService.cs +++ b/src/Features/Core/Portable/SymbolSearch/Windows/IDatabaseFactoryService.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.IO; using Microsoft.CodeAnalysis.Elfie.Model; namespace Microsoft.CodeAnalysis.SymbolSearch; internal interface IDatabaseFactoryService { - AddReferenceDatabase CreateDatabaseFromBytes(byte[] bytes, bool isBinary); + AddReferenceDatabase CreateDatabaseFromStream(Stream stream, bool isBinary); } diff --git a/src/Features/Core/Portable/SymbolSearch/Windows/IIOService.cs b/src/Features/Core/Portable/SymbolSearch/Windows/IIOService.cs index ae1b103c5262..4f642cfbc670 100644 --- a/src/Features/Core/Portable/SymbolSearch/Windows/IIOService.cs +++ b/src/Features/Core/Portable/SymbolSearch/Windows/IIOService.cs @@ -15,6 +15,7 @@ internal interface IIOService void Create(DirectoryInfo directory); void Delete(FileInfo file); bool Exists(FileSystemInfo info); + Stream OpenRead(string path); byte[] ReadAllBytes(string path); void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName, bool ignoreMetadataErrors); void Move(string sourceFileName, string destinationFileName); diff --git a/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.DatabaseFactoryService.cs b/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.DatabaseFactoryService.cs index a603461b1ca9..05b6c7b20b1a 100644 --- a/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.DatabaseFactoryService.cs +++ b/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.DatabaseFactoryService.cs @@ -11,19 +11,18 @@ internal sealed partial class SymbolSearchUpdateEngine { private sealed class DatabaseFactoryService : IDatabaseFactoryService { - public AddReferenceDatabase CreateDatabaseFromBytes(byte[] bytes, bool isBinary) + public AddReferenceDatabase CreateDatabaseFromStream(Stream stream, bool isBinary) { - using var memoryStream = new MemoryStream(bytes); var database = new AddReferenceDatabase(ArdbVersion.V1); if (isBinary) { - using var binaryReader = new BinaryReader(memoryStream); + using var binaryReader = new BinaryReader(stream); database.ReadBinary(binaryReader); } else { - using var streamReader = new StreamReader(memoryStream); + using var streamReader = new StreamReader(stream); database.ReadText(streamReader); } diff --git a/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.IOService.cs b/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.IOService.cs index 36b2b653ad2d..a82d7dcd1f29 100644 --- a/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.IOService.cs +++ b/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.IOService.cs @@ -17,6 +17,8 @@ private sealed class IOService : IIOService public bool Exists(FileSystemInfo info) => info.Exists; + public Stream OpenRead(string path) => File.OpenRead(path); + public byte[] ReadAllBytes(string path) => File.ReadAllBytes(path); public void Replace(string sourceFileName, string destinationFileName, string? destinationBackupFileName, bool ignoreMetadataErrors) diff --git a/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.Update.cs b/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.Update.cs index 3cdbe883397d..f3cfcb73a412 100644 --- a/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.Update.cs +++ b/src/Features/Core/Portable/SymbolSearch/Windows/SymbolSearchUpdateEngine.Update.cs @@ -273,7 +273,8 @@ private async Task DownloadFullDatabaseAsync(FileInfo databaseFileInfo // searching. try { - database = CreateAndSetInMemoryDatabase(bytes, isBinary: false); + using var stream = new MemoryStream(bytes); + database = CreateAndSetInMemoryDatabase(stream, isBinary: false); } catch (Exception e) when (_service._reportAndSwallowExceptionUnlessCanceled(e, cancellationToken)) { @@ -391,18 +392,18 @@ private async Task PatchLocalDatabaseAsync(FileInfo databaseFileInfo, LogInfo("Reading in local database"); - var (databaseBytes, isBinary) = GetDatabaseBytes(databaseFileInfo); + using var stream = GetDatabaseStream(databaseFileInfo, out var isBinary); - LogInfo($"Reading in local database completed. databaseBytes.Length={databaseBytes.Length}"); + LogInfo($"Reading in local database completed. isBinary={isBinary}"); - // Make a database instance out of those bytes and set is as the current in memory database - // that searches will run against. If we can't make a database instance from these bytes + // Make a database instance from the stream and set it as the current in memory database + // that searches will run against. If we can't make a database instance from the stream // then our local database is corrupt and we need to download the full database to get back // into a good state. AddReferenceDatabase database; try { - database = CreateAndSetInMemoryDatabase(databaseBytes, isBinary); + database = CreateAndSetInMemoryDatabase(stream, isBinary); } catch (Exception e) when (_service._reportAndSwallowExceptionUnlessCanceled(e, cancellationToken)) { @@ -424,7 +425,8 @@ private async Task PatchLocalDatabaseAsync(FileInfo databaseFileInfo, databaseFileInfo, element, // We pass a delegate to get the database bytes so that we can avoid reading the bytes when we don't need them due to no patch to apply. - getDatabaseBytes: () => isBinary ? _service._ioService.ReadAllBytes(databaseFileInfo.FullName) : databaseBytes, + // Note: ApplyPatch requires byte[], so we must read into memory here. + getDatabaseBytes: () => _service._ioService.ReadAllBytes(isBinary ? GetBinaryFileInfo(databaseFileInfo).FullName : databaseFileInfo.FullName), cancellationToken).ConfigureAwait(false); LogInfo("Downloading and processing patch file completed"); @@ -432,33 +434,35 @@ private async Task PatchLocalDatabaseAsync(FileInfo databaseFileInfo, return delayUntilUpdate; - (byte[] dataBytes, bool isBinary) GetDatabaseBytes(FileInfo databaseFileInfo) + Stream GetDatabaseStream(FileInfo databaseFileInfo, out bool isBinary) { var databaseBinaryFileInfo = GetBinaryFileInfo(databaseFileInfo); try { // First attempt to read from the binary file. If that fails, fall back to the text file. - return (_service._ioService.ReadAllBytes(databaseBinaryFileInfo.FullName), isBinary: true); + isBinary = true; + return _service._ioService.OpenRead(databaseBinaryFileInfo.FullName); } catch (Exception e) when (IOUtilities.IsNormalIOException(e)) { } // (intentionally not wrapped in IOUtilities. If this throws we want to restart). - return (_service._ioService.ReadAllBytes(databaseFileInfo.FullName), isBinary: false); + isBinary = false; + return _service._ioService.OpenRead(databaseFileInfo.FullName); } } /// - /// Creates a database instance with the bytes passed in. If creating the database succeeds, + /// Creates a database instance with the stream passed in. If creating the database succeeds, /// then it will be set as the current in memory version. In the case of failure (which /// indicates that our data is corrupt), the exception will bubble up and must be appropriately /// dealt with by the caller. /// - private AddReferenceDatabase CreateAndSetInMemoryDatabase(byte[] bytes, bool isBinary) + private AddReferenceDatabase CreateAndSetInMemoryDatabase(Stream stream, bool isBinary) { - var database = CreateDatabaseFromBytes(bytes, isBinary); + var database = CreateDatabaseFromStream(stream, isBinary); _service._sourceToDatabase[_source] = new AddReferenceDatabaseWrapper(database); return database; } @@ -520,7 +524,8 @@ private async Task ProcessPatchXElementAsync( LogInfo($"Applying patch completed. finalBytes.Length={finalBytes.Length}"); // finalBytes is generated from the current database and the patch, not from the binary file. - database = CreateAndSetInMemoryDatabase(finalBytes, isBinary: false); + using var stream = new MemoryStream(finalBytes); + database = CreateAndSetInMemoryDatabase(stream, isBinary: false); // Attempt to persist the txt and binary forms of the index. It's ok if either of these writes // don't succeed. If the txt file fails to persist, a subsequent VS session will redownload the @@ -560,11 +565,11 @@ private static void ParsePatchElement(XElement patchElement, out bool upToDate, } } - private AddReferenceDatabase CreateDatabaseFromBytes(byte[] bytes, bool isBinary) + private AddReferenceDatabase CreateDatabaseFromStream(Stream stream, bool isBinary) { - LogInfo("Creating database from bytes"); - var result = _service._databaseFactoryService.CreateDatabaseFromBytes(bytes, isBinary); - LogInfo("Creating database from bytes completed"); + LogInfo("Creating database from stream"); + var result = _service._databaseFactoryService.CreateDatabaseFromStream(stream, isBinary); + LogInfo("Creating database from stream completed"); return result; } diff --git a/src/Features/Core/Portable/TypeHierarchy/AbstractTypeHierarchyService.cs b/src/Features/Core/Portable/TypeHierarchy/AbstractTypeHierarchyService.cs new file mode 100644 index 000000000000..4cd880254542 --- /dev/null +++ b/src/Features/Core/Portable/TypeHierarchy/AbstractTypeHierarchyService.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.FindSymbols.FindReferences; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.TypeHierarchy; + +internal abstract class AbstractTypeHierarchyService : ITypeHierarchyService +{ + public ImmutableArray GetBaseTypesAndInterfaces(INamedTypeSymbol typeSymbol) + => BaseTypeFinder.FindBaseTypesAndInterfaces(typeSymbol); + + public async Task> GetDerivedTypesAndImplementationsAsync( + Solution solution, + INamedTypeSymbol typeSymbol, + bool transitive, + CancellationToken cancellationToken) + { + if (typeSymbol.IsInterfaceType()) + { + var allDerivedInterfaces = await SymbolFinder.FindDerivedInterfacesArrayAsync( + typeSymbol, + solution, + transitive: transitive, + cancellationToken: cancellationToken).ConfigureAwait(false); + var allImplementations = await SymbolFinder.FindImplementationsArrayAsync( + typeSymbol, + solution, + transitive: transitive, + cancellationToken: cancellationToken).ConfigureAwait(false); + return [.. allDerivedInterfaces, .. allImplementations]; + } + + return await SymbolFinder.FindDerivedClassesArrayAsync( + typeSymbol, + solution, + transitive: transitive, + cancellationToken: cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/Features/Core/Portable/TypeHierarchy/ITypeHierarchyService.cs b/src/Features/Core/Portable/TypeHierarchy/ITypeHierarchyService.cs new file mode 100644 index 000000000000..ed20ba2403d0 --- /dev/null +++ b/src/Features/Core/Portable/TypeHierarchy/ITypeHierarchyService.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.TypeHierarchy; + +internal interface ITypeHierarchyService : ILanguageService +{ + ImmutableArray GetBaseTypesAndInterfaces(INamedTypeSymbol typeSymbol); + + Task> GetDerivedTypesAndImplementationsAsync( + Solution solution, + INamedTypeSymbol typeSymbol, + bool transitive, + CancellationToken cancellationToken); +} diff --git a/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs b/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs index da99a0982af7..4f5263ddc77f 100644 --- a/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs +++ b/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs @@ -15,6 +15,13 @@ namespace Microsoft.CodeAnalysis.Features.Workspaces; internal static class MiscellaneousFileUtilities { + internal static bool IsScriptFile(LanguageInformation languageInformation, string filePath) + { + return languageInformation.ScriptExtension is not null + && PathUtilities.GetExtension(filePath) == languageInformation.ScriptExtension; + } + + /// Whether the host has globally enabled the C# file-based programs feature. internal static ProjectInfo CreateMiscellaneousProjectInfoForDocument( Workspace workspace, string filePath, @@ -22,9 +29,9 @@ internal static ProjectInfo CreateMiscellaneousProjectInfoForDocument( LanguageInformation languageInformation, SourceHashAlgorithm checksumAlgorithm, SolutionServices services, - ImmutableArray metadataReferences) + ImmutableArray metadataReferences, + bool enableFileBasedPrograms) { - var fileExtension = PathUtilities.GetExtension(filePath); var fileName = PathUtilities.GetFileName(filePath); var languageName = languageInformation.LanguageName; @@ -48,15 +55,15 @@ internal static ProjectInfo CreateMiscellaneousProjectInfoForDocument( if (parseOptions != null) { if (compilationOptions != null && - languageInformation.ScriptExtension is not null && - fileExtension == languageInformation.ScriptExtension) + IsScriptFile(languageInformation, filePath)) { parseOptions = parseOptions.WithKind(SourceCodeKind.Script); compilationOptions = GetCompilationOptionsWithScriptReferenceResolvers(services, compilationOptions, filePath); } - else + else if (enableFileBasedPrograms) { - // Any non-script misc file should not complain about usage of '#:' ignored directives. + // The host has enabled the file-based programs feature, so + // any non-script misc file should not complain about usage of '#:' ignored directives. parseOptions = parseOptions.WithFeatures([.. parseOptions.Features, new("FileBasedProgram", "true")]); } } diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf index 9f47a7791dd2..16c5fa0281ca 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf @@ -460,17 +460,22 @@ Ujistěte se, že specifikátor tt použijete pro jazyky, pro které je nezbytn Změna nastavení projektu {0} z {1} na {2} vyžaduje restartování aplikace. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application Změna pseudovlastního atributu {0} u {1} vyžaduje restartování aplikace. + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + Změna zdrojového souboru {0} v projektu {1}, který nepodporuje Opětovné načítání za provozu vyžaduje restartování aplikace; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + Změna zdrojového souboru {0} v zastaralém projektu {1} nemá žádný vliv, dokud projekt nebude znovu sestaven; {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application Změna nadřazeného oboru názvů {0} z {1} na {2} vyžaduje restartování aplikace. @@ -3170,6 +3175,11 @@ Pozitivní kontrolní výrazy zpětného vyhledávání s nulovou délkou se obv {0} - {1} + + {0} does not support Hot Reload. + {0} nepodporuje Opětovné načítání za provozu. + + '{0}' expected Očekával se {0} @@ -4432,17 +4442,17 @@ Když se tento specifikátor standardního formátu použije, operace formátov the content of the document is stale. - the content of the document is stale. + obsah dokumentu je zastaralý. the project contains stale document '{0}'. - the project contains stale document '{0}'. + projekt obsahuje zastaralý dokument {0}. the project has not been built. - the project has not been built. + projekt nebyl sestaven. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf index b35db842bebd..749b6d5da3bf 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf @@ -460,17 +460,22 @@ Stellen Sie sicher, dass Sie den Bezeichner "tt" für Sprachen verwenden, für d Zum Ändern der Projekteinstellung „{0}“ von „{1}“ in „{2}“ muss die Anwendung neu gestartet werden. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application Das Ändern des pseudobenutzerdefinierten Attributs „{0}“ von {1} erfordert einen Neustart der Anwendung. + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + Das Ändern der Quelldatei „{0}“ in einem Projekt „{1}“, das Hot Reload nicht unterstützt, erfordert einen Neustart der Anwendung; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + Das Ändern der Quelldatei „{0}“ in einem veralteten Projekt „{1}“ hat keine Wirkung, bis das Projekt neu erstellt wird; {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application Um den enthaltenden Namespace von "{0}" von "{1}" in "{2}" zu ändern, muss die Anwendung neu gestartet werden @@ -3170,6 +3175,11 @@ Positive Lookbehindassertionen mit Nullbreite werden normalerweise am Anfang reg {0} - {1} + + {0} does not support Hot Reload. + {0} unterstützt Hot Reload nicht. + + '{0}' expected '{0}' erwartet @@ -4432,17 +4442,17 @@ Bei Verwendung dieses Standardformatbezeichners wird zur Formatierung oder Analy the content of the document is stale. - the content of the document is stale. + der Inhalt des Dokuments ist veraltet. the project contains stale document '{0}'. - the project contains stale document '{0}'. + das Projekt enthält das veraltete Dokument „{0}“. the project has not been built. - the project has not been built. + das Projekt wurde nicht erstellt. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf index f1e2166e050c..412fe666f144 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf @@ -460,17 +460,22 @@ Asegúrese de usar el especificador "tt" para los idiomas para los que es necesa Cambiar el valor del proyecto “{0}” de “{1}” a “{2}” requiere reiniciar la aplicación. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application Para cambiar el atributo pseudo-personalizado "{0}" de {1} se requiere reiniciar la aplicación + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + Cambiar el archivo de origen ''{0}'' en un proyecto ''{1}'' que no admite Recarga activa requiere reiniciar la aplicación; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + Cambiar el archivo de origen “{0}” en un proyecto obsoleto {1} no tiene efecto hasta que se reconstruya el proyecto; {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application Para cambiar el espacio de nombres contenedor de '{0}' de '{1}' a '{2}' es necesario reiniciar la aplicación @@ -3170,6 +3175,11 @@ Las aserciones de búsqueda retrasada (lookbehind) positivas de ancho cero se us {0} - {1} + + {0} does not support Hot Reload. + {0} no admite Recarga activa. + + '{0}' expected \"{0}\" esperado @@ -4432,17 +4442,17 @@ Cuando se usa este especificador de formato estándar, la operación de formato the content of the document is stale. - the content of the document is stale. + el contenido del documento está obsoleto. the project contains stale document '{0}'. - the project contains stale document '{0}'. + el proyecto contiene el documento obsoleto ''{0}''. the project has not been built. - the project has not been built. + el proyecto no se ha compilado. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf index 762097814629..248ae4541012 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf @@ -460,17 +460,22 @@ Veillez à utiliser le spécificateur "tt" pour les langues où il est nécessai Le remplacement du paramètre de projet « {0} » de « {1} » par « {2} » nécessite le redémarrage de l’application. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application La modification de l’attribut pseudo-personnalisé « {0} » de {1} requiert le redémarrage de l’application + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + Modifier le fichier source « {0} » dans un projet « {1} » qui ne prend pas en charge le rechargement à chaud nécessite de redémarrer l’application; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + La modification du fichier source « {0} » dans un projet obsolète « {1} » n’a aucun effet tant que le projet n’est pas reconstruit, {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application La modification de l’espace de noms conteneur de '{0}' de '{1}' en '{2}' nécessite le redémarrage de l’application @@ -3170,6 +3175,11 @@ Les assertions arrière positives de largeur nulle sont généralement utilisée {0} - {1} + + {0} does not support Hot Reload. + {0} ne prend pas en charge le rechargement à chaud. + + '{0}' expected '{0}' attendu @@ -4432,17 +4442,17 @@ Quand ce spécificateur de format standard est utilisé, l'opération qui consis the content of the document is stale. - the content of the document is stale. + le contenu du document est obsolète. the project contains stale document '{0}'. - the project contains stale document '{0}'. + le projet contient le document périmé « {0} ». the project has not been built. - the project has not been built. + le projet n'a pas été généré. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf index 06334e6d2f0e..ac8a33dfbab7 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf @@ -460,17 +460,22 @@ Assicurarsi di usare l'identificatore "tt" per le lingue per le quali è necessa La modifica dell'impostazione del progetto '{0}' da '{1}' a '{2}' richiede il riavvio dell'applicazione. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application Se si modifica l'attributo pseudo-personalizzato '{0}' di {1}, è necessario riavviare l'applicazione + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + La modifica del file di origine "{0}"' in un progetto "{1}" che non supporta il Ricaricamento rapido richiede il riavvio dell'applicazione; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + La modifica del file di origine '{0}' in un progetto non aggiornato '{1}' non ha alcun effetto finché il progetto non viene ricompilato; {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application Per modificare lo spazio dei nomi contenitore di '{0}' da '{1}' a '{2}' è necessario riavviare l'applicazione @@ -3170,6 +3175,11 @@ Le asserzioni lookbehind positive di larghezza zero vengono usate in genere all' {0} - {1} + + {0} does not support Hot Reload. + {0} non supporta il Ricaricamento rapido. + + '{0}' expected '{0}' previsto @@ -4432,17 +4442,17 @@ Quando si usa questo identificatore di formato standard, la formattazione o l'op the content of the document is stale. - the content of the document is stale. + il contenuto del documento non è aggiornato. the project contains stale document '{0}'. - the project contains stale document '{0}'. + il progetto contiene il documento non aggiornato '{0}'. the project has not been built. - the project has not been built. + questo progetto non è stato compilato. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf index ba93993fb9bc..cd57da96cca6 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf @@ -460,17 +460,22 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma プロジェクト設定 '{0}' を '{1}' から '{2}' に変更するには、アプリケーションを再起動する必要があります。 - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application {0} の擬似カスタム属性 '{1}' を変更するには、アプリケーションを再起動する必要があります + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + ホット リロードをサポートしていないプロジェクト '{1}' 内のソース ファイル '{0}' を変更すると、アプリケーションを再起動する必要があります。{2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + 古いプロジェクト '{1}' でソース ファイル '{0}' を変更しても、プロジェクトが再構築されるまで影響がありません。{2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application '{0}' の包含名前空間を '{1}' から '{2}' に変更するには、アプリケーションを再起動する必要があります @@ -3170,6 +3175,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of {0} - {1} + + {0} does not support Hot Reload. + {0} ではホット リロードはサポートされていません。 + + '{0}' expected '{0}' が必要です @@ -4432,17 +4442,17 @@ When this standard format specifier is used, the formatting or parsing operation the content of the document is stale. - the content of the document is stale. + ドキュメントのコンテンツが古くなっています。 the project contains stale document '{0}'. - the project contains stale document '{0}'. + プロジェクトに古いドキュメント '{0}' が含まれています。 the project has not been built. - the project has not been built. + プロジェクトは構築されていません。 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf index 08670f6d2e04..d4152d157191 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf @@ -460,17 +460,22 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma 프로젝트 설정 '{0}'을(를) '{1}'에서 '{2}'(으)로 변경하면 애플리케이션을 다시 시작해야 합니다. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application {1}의 허위 사용자 지정 특성 '{0}'을(를) 변경하려면 애플리케이션을 다시 시작해야 합니다. + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + 핫 다시 로드 지원하지 않는 프로젝트 '{1}'에서 소스 파일 '{0}'을(를) 변경하려면 애플리케이션을 다시 시작해야 합니다. {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + 오래된 프로젝트 '{1}'에서 소스 파일 '{0}'을(를) 변경해도 프로젝트를 다시 빌드하기 전까지는 적용되지 않습니다. {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application '{0}'의 포함 네임스페이스를 '{1}'에서 '{2}'(으)로 변경하려면 애플리케이션을 다시 시작해야 합니다. @@ -3170,6 +3175,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of {0} - {1} + + {0} does not support Hot Reload. + {0}은(는) 핫 다시 로드를 지원하지 않습니다. + + '{0}' expected '{0}'이(가) 필요합니다. @@ -4432,17 +4442,17 @@ When this standard format specifier is used, the formatting or parsing operation the content of the document is stale. - the content of the document is stale. + 문서 내용이 오래되었습니다. the project contains stale document '{0}'. - the project contains stale document '{0}'. + 프로젝트에 오래된 문서 '{0}'(이)가 포함되어 있습니다. the project has not been built. - the project has not been built. + 프로젝트가 빌드되지 않았습니다. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf index 90cc5b2f61c8..0461ffa9ed8e 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf @@ -460,17 +460,22 @@ Pamiętaj, aby nie używać specyfikatora „tt” dla wszystkich języków, w k Zmiana ustawienia projektu „{0}” z „{1}” na „{2}” wymaga ponownego uruchomienia aplikacji. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application Zmiana atrybutu pseudoniestandardowego elementu „{0}” z {1} wymaga ponownego uruchomienia aplikacji + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + Zmiana pliku źródłowego „{0}” w projekcie „{1}”, który nie obsługuje przeładowywania na gorąco, wymaga ponownego uruchomienia aplikacji; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + Zmiana pliku źródłowego „{0}” w nieaktualnym projekcie „{1}” nie ma żadnego efektu, dopóki projekt nie zostanie przebudowany; {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application Zmiana zawierającej przestrzeni nazw „{0}” z „{1}” na „{2}” wymaga ponownego uruchomienia aplikacji @@ -3170,6 +3175,11 @@ Pozytywne asercje wsteczne o zerowej szerokości są zwykle używane na początk {0} - {1} + + {0} does not support Hot Reload. + {0} nie obsługuje przeładowywania na gorąco. + + '{0}' expected Oczekiwano: „{0}” @@ -4432,17 +4442,17 @@ Gdy jest używany ten standardowy specyfikator formatu, operacja formatowania lu the content of the document is stale. - the content of the document is stale. + zawartość dokumentu jest nieaktualna. the project contains stale document '{0}'. - the project contains stale document '{0}'. + projekt zawiera nieaktualny dokument „{0}”. the project has not been built. - the project has not been built. + projekt nie został skompilowany. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf index a8f3815d08f7..4a729f6afbf6 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf @@ -460,17 +460,22 @@ Verifique se o especificador "tt" foi usado para idiomas para os quais é necess Alterar a configuração do projeto "{0}" de "{1}" para "{2}" requer a reinicialização do aplicativo. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application Alterar o atributo pseudo-personalizado '{0}' de {1} requer o reinício do aplicativo + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + Alterar o arquivo de origem ''{0}'' em um projeto ''{1}'' que não dá suporte à Recarga Dinâmica exige reiniciar o aplicativo; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + Alterar o arquivo de origem '{0}' em um projeto obsoleto '{1}' não tem efeito até que o projeto seja reconstruído; {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application Alterar o namespace que o contém '{0}' de '{1}' para '{2}' requer a reinicialização do aplicativo @@ -3170,6 +3175,11 @@ As declarações de lookbehind positivas de largura zero normalmente são usadas {0} - {1} + + {0} does not support Hot Reload. + {0} não dá suporte à Recarga Dinâmica. + + '{0}' expected “{0}” esperado @@ -4432,17 +4442,17 @@ Quando esse especificador de formato padrão é usado, a operação de análise the content of the document is stale. - the content of the document is stale. + o conteúdo do documento está obsoleto. the project contains stale document '{0}'. - the project contains stale document '{0}'. + o projeto contém o documento obsoleto '{0}'. the project has not been built. - the project has not been built. + o projeto não foi compilado. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf index 34375634e451..8baddbe1c42c 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf @@ -460,17 +460,22 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma Чтобы изменить параметр проекта "{0}" с "{1}" на "{2}", необходимо перезапустить приложение. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application Для изменения псевдонастраиваемого атрибута "{0}" для {1} требуется перезапустить приложение. + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + Для изменения исходного файла "{0}" в проекте "{1}", который не поддерживает Горячую перезагрузку, требуется перезапустить приложение; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + Изменение исходного файла "{0}" в устаревшем проекте "{1}" вступит в силу только после повторной сборки проекта. {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application Чтобы изменить содержащее пространство имен "{0}" с "{1}" на "{2}", необходимо перезапустить приложение @@ -3170,6 +3175,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of {0} - {1} + + {0} does not support Hot Reload. + {0} не поддерживает Горячую перезагрузку. + + '{0}' expected Ожидалось значение {0} @@ -4432,17 +4442,17 @@ When this standard format specifier is used, the formatting or parsing operation the content of the document is stale. - the content of the document is stale. + Содержимое документа устарело. the project contains stale document '{0}'. - the project contains stale document '{0}'. + Проект содержит устаревший документ "{0}". the project has not been built. - the project has not been built. + Проект не был собран. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf index d7308abca116..b7eb3ad88248 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf @@ -460,17 +460,22 @@ AM ve PM arasındaki farkın korunmasının gerekli olduğu diller için "tt" be '{1}' olan '{0}' proje ayarını '{2}' olarak değiştirmek için uygulamanın yeniden başlatılması gerekiyor. - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application {1} öğesinin '{0}' sahte özel özniteliğinin değiştirilmesi, uygulamanın yeniden başlatılmasını gerektirir + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + Çalışırken Yeniden Yüklemeyi desteklemeyen '{1}' projesindeki '{0}' kaynak dosyasını değiştirmek için uygulamanın yeniden başlatılması gerekiyor; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + Eski bir projedeki ('{1}') kaynak dosyayı ,('{0}') değiştirmenin, proje yeniden derlenene kadar hiçbir etkisi olmaz; {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application '{1}' olan '{0}' kapsayan ad alanının '{2}' olarak değiştirilmesi, uygulamanın yeniden başlatılmasını gerektirir @@ -3170,6 +3175,11 @@ Sıfır genişlikli pozitif geri yönlü onaylamalar genellikle normal ifadeleri {0} - {1} + + {0} does not support Hot Reload. + {0}, Çalışırken Yeniden Yüklemeyi desteklemiyor. + + '{0}' expected '{0}' bekleniyor @@ -4432,17 +4442,17 @@ Bu standart biçim belirticisi kullanıldığında, biçimlendirme veya ayrışt the content of the document is stale. - the content of the document is stale. + belgenin içeriği güncel değil. the project contains stale document '{0}'. - the project contains stale document '{0}'. + proje eski belge '{0}' içeriyor. the project has not been built. - the project has not been built. + proje oluşturulmadı. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf index 519084e476aa..a9d6ce4fc72a 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf @@ -460,17 +460,22 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma 将项目设置 ‘{0}’ 从 ‘{1}’ 更改为 ‘{2}’ 需要重启应用程序。 - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application 更改 {1} 的伪自定义属性“{0}”需要重启应用程序 + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + 在项目 "{1}" 中更改不支持热重载的源文件 "{0}" 需要重新启动应用程序;{2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + 在重新生成项目之前,更改过时项目 "{1}" 中的源文件 "{0}" 将不起作用;{2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application 将 '{0}' 的包含命名空间从 '{1}' 更改为'{2}' 需要重新启动应用程序 @@ -3170,6 +3175,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of {0} - {1} + + {0} does not support Hot Reload. + {0} 不支持热重载。 + + '{0}' expected 应为 \"{0}\" @@ -4432,17 +4442,17 @@ When this standard format specifier is used, the formatting or parsing operation the content of the document is stale. - the content of the document is stale. + 文档内容已过时。 the project contains stale document '{0}'. - the project contains stale document '{0}'. + 项目包含过时文档 "{0}"。 the project has not been built. - the project has not been built. + 项目尚未生成。 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf index 656e697be042..61159e9aa54e 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf @@ -460,17 +460,22 @@ Make sure to use the "tt" specifier for languages for which it's necessary to ma 將專案設定 '{0}' 從 '{1}' 變更為 '{2}' 需要重新啟動應用程式。 - + Changing pseudo-custom attribute '{0}' of {1} requires restarting the application 變更 {1} 的虛擬自訂屬性 '{0}' 需要重新啟動應用程式 + + Changing source file '{0}' in a project '{1}' that does not support Hot Reload requires restarting the application; {2} + 變更不支援熱重新載入的專案 '{1}' 中的來源檔案 '{0}' 需要重新啟動應用程式; {2} + + Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} - Changing source file '{0}' in a stale project '{1}' has no effect until the project is rebuit; {2} + 在專案重新建置之前,於過時的專案 '{1}' 中變更來源檔案 '{0}' 不會產生效果; {2} - + Changing the containing namespace of '{0}' from '{1}' to '{2}' requires restarting the application 將包含的命名空間 '{0}' 從 '{1}' 變更為 '{2}' 需要重新啟動應用程式 @@ -3170,6 +3175,11 @@ Zero-width positive lookbehind assertions are typically used at the beginning of {0} - {1} + + {0} does not support Hot Reload. + {0} 不支援熱重新載入。 + + '{0}' expected 預期為 '{0}' @@ -4432,17 +4442,17 @@ When this standard format specifier is used, the formatting or parsing operation the content of the document is stale. - the content of the document is stale. + 文件內容已過時。 the project contains stale document '{0}'. - the project contains stale document '{0}'. + 專案包含過時的文件 '{0}'。 the project has not been built. - the project has not been built. + 專案尚未建置。 diff --git a/src/Features/ExternalAccess/HotReload/Api/HotReloadMSBuildWorkspace.ProjectFileInfoProvider.cs b/src/Features/ExternalAccess/HotReload/Api/HotReloadMSBuildWorkspace.ProjectFileInfoProvider.cs index c690a1c8dafd..1a71af9e6177 100644 --- a/src/Features/ExternalAccess/HotReload/Api/HotReloadMSBuildWorkspace.ProjectFileInfoProvider.cs +++ b/src/Features/ExternalAccess/HotReload/Api/HotReloadMSBuildWorkspace.ProjectFileInfoProvider.cs @@ -31,10 +31,10 @@ public Task> LoadProjectFileInfosAsync(string pr return Task.FromResult(instances.SelectAsArray(instance => { - var reader = new ProjectInstanceReader( - ProjectCommandLineProvider.Create(languageName), - instance, - project); + // dotnet-watch only supports C# projects for Hot Reload and ensures that C# language services are available: + var commandLineReader = ProjectCommandLineProvider.TryCreate(languageName, knownCommandLineParserLanguages: [LanguageNames.CSharp]); + + var reader = new ProjectInstanceReader(languageName, commandLineReader, instance, project); return reader.CreateProjectFileInfo(); })); diff --git a/src/Features/ExternalAccess/HotReload/Api/HotReloadMSBuildWorkspace.cs b/src/Features/ExternalAccess/HotReload/Api/HotReloadMSBuildWorkspace.cs index 1d1842f10535..64b24c023f24 100644 --- a/src/Features/ExternalAccess/HotReload/Api/HotReloadMSBuildWorkspace.cs +++ b/src/Features/ExternalAccess/HotReload/Api/HotReloadMSBuildWorkspace.cs @@ -46,14 +46,21 @@ public HotReloadMSBuildWorkspace(ILogger logger, Func UpdateProjectConeAsync(string projectPath, CancellationToken cancellationToken) + // TODO: remove + public ValueTask UpdateProjectConeAsync(string projectPath, CancellationToken cancellationToken) + => UpdateProjectGraphAsync([projectPath], cancellationToken); + + /// + /// Updates all projects in the workspace whose file paths are specified in and all their transitive dependencies. + /// + public async ValueTask UpdateProjectGraphAsync(ImmutableArray projectPaths, CancellationToken cancellationToken) { - Contract.ThrowIfFalse(Path.IsPathFullyQualified(projectPath)); + Contract.ThrowIfFalse(projectPaths.All(Path.IsPathFullyQualified)); var projectMap = ProjectMap.Create(); var projectInfos = await _loader.LoadInfosAsync( - [projectPath], + projectPaths, _projectGraphFileInfoProvider, projectMap, progress: null, diff --git a/src/Features/ExternalAccess/HotReload/InternalAPI.Unshipped.txt b/src/Features/ExternalAccess/HotReload/InternalAPI.Unshipped.txt index 1d37becdc6d7..cf803fd095c3 100644 --- a/src/Features/ExternalAccess/HotReload/InternalAPI.Unshipped.txt +++ b/src/Features/ExternalAccess/HotReload/InternalAPI.Unshipped.txt @@ -7,6 +7,7 @@ Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadMSBuildWorkspace Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadMSBuildWorkspace.HotReloadMSBuildWorkspace(Microsoft.Extensions.Logging.ILogger! logger, System.Func instances, Microsoft.Build.Evaluation.Project? project)>! getBuildProjects) -> void Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadMSBuildWorkspace.UpdateFileContentAsync(System.Collections.Generic.IEnumerable<(string! path, Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadFileChangeKind change)>! changedFiles, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadMSBuildWorkspace.UpdateProjectConeAsync(string! projectPath, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadMSBuildWorkspace.UpdateProjectGraphAsync(System.Collections.Immutable.ImmutableArray projectPaths, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadMSBuildWorkspace.UpdateSolution(System.Collections.Immutable.ImmutableArray projectInfos) -> Microsoft.CodeAnalysis.Solution! Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadService Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadService.CapabilitiesChanged() -> void @@ -62,6 +63,7 @@ static Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadService.GetT static Microsoft.CodeAnalysis.ExternalAccess.HotReload.Api.HotReloadService.WithProjectInfo(Microsoft.CodeAnalysis.Solution! solution, Microsoft.CodeAnalysis.ProjectInfo! info) -> Microsoft.CodeAnalysis.Solution! static Microsoft.CodeAnalysis.ExternalAccess.HotReload.Internal.ContractConversions.Convert(this Microsoft.CodeAnalysis.MSBuild.DocumentFileInfo! info) -> Microsoft.CodeAnalysis.MSBuild.DocumentFileInfo! static Microsoft.CodeAnalysis.ExternalAccess.HotReload.Internal.ContractConversions.Convert(this Microsoft.CodeAnalysis.MSBuild.FileGlobs! globs) -> Microsoft.CodeAnalysis.MSBuild.FileGlobs! -static Microsoft.CodeAnalysis.ExternalAccess.HotReload.Internal.ContractConversions.Convert(this Microsoft.CodeAnalysis.MSBuild.PackageReference! reference) -> Microsoft.CodeAnalysis.MSBuild.PackageReference! +static Microsoft.CodeAnalysis.ExternalAccess.HotReload.Internal.ContractConversions.Convert(this Microsoft.CodeAnalysis.MSBuild.MetadataReferenceItem reference) -> Microsoft.CodeAnalysis.MSBuild.MetadataReferenceItem +static Microsoft.CodeAnalysis.ExternalAccess.HotReload.Internal.ContractConversions.Convert(this Microsoft.CodeAnalysis.MSBuild.PackageReferenceItem! reference) -> Microsoft.CodeAnalysis.MSBuild.PackageReferenceItem! static Microsoft.CodeAnalysis.ExternalAccess.HotReload.Internal.ContractConversions.Convert(this Microsoft.CodeAnalysis.MSBuild.ProjectFileInfo! info) -> Microsoft.CodeAnalysis.MSBuild.ProjectFileInfo! static Microsoft.CodeAnalysis.ExternalAccess.HotReload.Internal.ContractConversions.Convert(this Microsoft.CodeAnalysis.MSBuild.ProjectFileReference! reference) -> Microsoft.CodeAnalysis.MSBuild.ProjectFileReference! diff --git a/src/Features/ExternalAccess/HotReloadTest/HotReloadServiceTests.cs b/src/Features/ExternalAccess/HotReloadTest/HotReloadServiceTests.cs index 4f887ea949f1..2a63a717e875 100644 --- a/src/Features/ExternalAccess/HotReloadTest/HotReloadServiceTests.cs +++ b/src/Features/ExternalAccess/HotReloadTest/HotReloadServiceTests.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.UnitTests; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; using Xunit; @@ -293,9 +294,14 @@ public async Task AnalyzerConfigFile() InspectDiagnostics(result.TransientDiagnostics)); } - [Fact] - public async Task StaleSource() + [Theory] + [InlineData(LanguageNames.CSharp)] + [InlineData(NoCompilationConstants.LanguageName)] + public async Task StaleSource(string language) { + // When testing non-C# projects we compile the code using C# compiler but pretend it's not a C# assembly. + // The actual content of the assembly is irrelevant to the test as long as the PDB has correct document checksums. + var source1 = "class C { void M() { System.Console.WriteLine(1); } }"; var source2 = "class C { void M() { System.Console.WriteLine(2); } }"; var source3 = "class C { void M() { System.Console.WriteLine(3); } }"; @@ -305,7 +311,7 @@ public async Task StaleSource() using var workspace = CreateWorkspace(out var solution, out _); solution = solution. - AddTestProject("P", out var projectId). + AddTestProject("P", language: language, out var projectId). AddTestDocument(source: null, sourceFileA.Path, out var documentIdA).Project.Solution; EmitLibrary(projectId, source1, sourceFileA.Path, assemblyName: "Proj"); @@ -387,5 +393,33 @@ public async Task StaleSource_AdditionalFile() [$"A: {generatedDoc.FilePath}: (0,26)-(0,42): Error ENC0023: {string.Format(FeaturesResources.Adding_an_abstract_0_or_overriding_an_inherited_0_requires_restarting_the_application, FeaturesResources.method)}"], InspectDiagnostics(result.TransientDiagnostics)); } + + [Theory] + [InlineData(LanguageNames.VisualBasic)] + [InlineData(LanguageNames.CSharp)] + [InlineData(NoCompilationConstants.LanguageName)] + public async Task HydrateDocumentsAsync(string language) + { + var dir = Temp.CreateDirectory(); + var sourceFileA = dir.CreateFile("A.nolang"); + + using var workspace = CreateWorkspace(out var solution, out _, [typeof(NoCompilationLanguageService)]); + + solution = solution. + AddTestProject("P", language, out var projectId). + AddTestDocument(source: null, sourceFileA.Path, out var documentIdA).Project.Solution; + + sourceFileA.WriteAllText("source", Encoding.UTF8); + + var hotReload = new HotReloadService(workspace.Services, ["Baseline"]); + + await hotReload.StartSessionAsync(solution, CancellationToken.None); + + // text should be loaded from disk: + var document = solution.GetRequiredDocument(documentIdA); + document.TryGetText(out var text); + Assert.NotNull(text); + Assert.Equal("source", text.ToString()); + } } #endif diff --git a/src/Features/Lsif/GeneratorTest/CompilerInvocationTests.vb b/src/Features/Lsif/GeneratorTest/CompilerInvocationTests.vb deleted file mode 100644 index afc954718d36..000000000000 --- a/src/Features/Lsif/GeneratorTest/CompilerInvocationTests.vb +++ /dev/null @@ -1,179 +0,0 @@ -' Licensed to the .NET Foundation under one or more agreements. -' The .NET Foundation licenses this file to you under the MIT license. -' See the LICENSE file in the project root for more information. - -Imports Microsoft.CodeAnalysis.Collections -Imports Microsoft.CodeAnalysis.Test.Utilities - -Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests - - Public Class CompilerInvocationTests - - Public Async Function TestCSharpProject() As Task - ' PortableExecutableReference.CreateFromFile implicitly reads the file so the file must exist. - Dim referencePath = GetType(Object).Assembly.Location - - Dim project = Await Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.CompilerInvocation.CreateFromJsonAsync(" - { - ""tool"": ""csc"", - ""arguments"": ""/noconfig /nowarn:1701,1702 /fullpaths /define:DEBUG /reference:" + referencePath.Replace("\", "\\") + " Z:\\SourceFile.cs /target:library /out:Z:\\Output.dll"", - ""projectFilePath"": ""Z:\\Project.csproj"", - ""sourceRootPath"": ""Z:\\"" - }") - - Assert.Equal(LanguageNames.CSharp, project.Language) - Assert.Equal("Z:\Project.csproj", project.FilePath) - Dim compilation = Await project.GetCompilationAsync() - Assert.Equal(OutputKind.DynamicallyLinkedLibrary, compilation.Options.OutputKind) - - Dim syntaxTree = Assert.Single(compilation.SyntaxTrees) - Assert.Equal("Z:\SourceFile.cs", syntaxTree.FilePath) - Assert.Equal("DEBUG", Assert.Single(syntaxTree.Options.PreprocessorSymbolNames)) - - Dim metadataReference = Assert.Single(compilation.References) - - Assert.Equal(referencePath, DirectCast(metadataReference, PortableExecutableReference).FilePath) - End Function - - - Public Async Function TestVisualBasicProject() As Task - ' PortableExecutableReference.CreateFromFile implicitly reads the file so the file must exist. - Dim referencePath = GetType(Object).Assembly.Location - - Dim project = Await Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.CompilerInvocation.CreateFromJsonAsync(" - { - ""tool"": ""vbc"", - ""arguments"": ""/noconfig /nowarn:1701,1702 /fullpaths /define:DEBUG /reference:" + referencePath.Replace("\", "\\") + " Z:\\SourceFile.vb /target:library /out:Z:\\Output.dll"", - ""projectFilePath"": ""Z:\\Project.vbproj"", - ""sourceRootPath"": ""Z:\\"" - }") - - Assert.Equal(LanguageNames.VisualBasic, project.Language) - Assert.Equal("Z:\Project.vbproj", project.FilePath) - Dim compilation = Await project.GetCompilationAsync() - Assert.Equal(OutputKind.DynamicallyLinkedLibrary, compilation.Options.OutputKind) - - Dim syntaxTree = Assert.Single(compilation.SyntaxTrees) - Assert.Equal("Z:\SourceFile.vb", syntaxTree.FilePath) - Assert.Contains("DEBUG", syntaxTree.Options.PreprocessorSymbolNames) - - Dim metadataReference = Assert.Single(compilation.References) - - Assert.Equal(referencePath, DirectCast(metadataReference, PortableExecutableReference).FilePath) - End Function - - - - Public Async Function TestSourceFilePathMappingWithDriveLetters( from As String, [to] As String) As Task - Dim project = Await Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.CompilerInvocation.CreateFromJsonAsync(" - { - ""tool"": ""csc"", - ""arguments"": ""/noconfig /nowarn:1701,1702 /fullpaths /define:DEBUG F:\\SourceFile.cs /target:library /out:F:\\Output.dll"", - ""projectFilePath"": ""F:\\Project.csproj"", - ""sourceRootPath"": ""F:\\"", - ""pathMappings"": [ - { - ""from"": """ + from.Replace("\", "\\") + """, - ""to"": """ + [to].Replace("\", "\\") + """ - }] - }") - - Dim compilation = Await project.GetCompilationAsync() - Dim syntaxTree = Assert.Single(compilation.SyntaxTrees) - - Assert.Equal("T:\SourceFile.cs", syntaxTree.FilePath) - End Function - - - Public Async Function TestSourceFilePathMappingWithSubdirectoriesWithoutTrailingSlashes() As Task - Dim project = Await Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.CompilerInvocation.CreateFromJsonAsync(" - { - ""tool"": ""csc"", - ""arguments"": ""/noconfig /nowarn:1701,1702 /fullpaths /define:DEBUG F:\\Directory\\SourceFile.cs /target:library /out:F:\\Output.dll"", - ""projectFilePath"": ""F:\\Project.csproj"", - ""sourceRootPath"": ""F:\\"", - ""pathMappings"": [ - { - ""from"": ""F:\\Directory"", - ""to"": ""T:\\Directory"" - }] - }") - - Dim compilation = Await project.GetCompilationAsync() - Dim syntaxTree = Assert.Single(compilation.SyntaxTrees) - - Assert.Equal("T:\Directory\SourceFile.cs", syntaxTree.FilePath) - End Function - - - Public Async Function TestSourceFilePathMappingWithSubdirectoriesWithDoubleSlashesInFilePath() As Task - Dim project = Await Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.CompilerInvocation.CreateFromJsonAsync(" - { - ""tool"": ""csc"", - ""arguments"": ""/noconfig /nowarn:1701,1702 /fullpaths /define:DEBUG F:\\Directory\\\\SourceFile.cs /target:library /out:F:\\Output.dll"", - ""projectFilePath"": ""F:\\Project.csproj"", - ""sourceRootPath"": ""F:\\"", - ""pathMappings"": [ - { - ""from"": ""F:\\Directory"", - ""to"": ""T:\\Directory"" - }] - }") - - Dim compilation = Await project.GetCompilationAsync() - Dim syntaxTree = Assert.Single(compilation.SyntaxTrees) - - Assert.Equal("T:\Directory\SourceFile.cs", syntaxTree.FilePath) - End Function - - - Public Async Function TestRuleSetPathMapping() As Task - Const RuleSetContents = " - - - - -" - - Using ruleSet = New DisposableFile(extension:=".ruleset") - ruleSet.WriteAllText(RuleSetContents) - - ' We will test that if we redirect the ruleset to the temporary file that we wrote that the values are still read. - Dim project = Await Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.CompilerInvocation.CreateFromJsonAsync(" - { - ""tool"": ""csc"", - ""arguments"": ""/noconfig /nowarn:1701,1702 /fullpaths /define:DEBUG /ruleset:F:\\Ruleset.ruleset /out:Output.dll"", - ""projectFilePath"": ""F:\\Project.csproj"", - ""sourceRootPath"": ""F:\\"", - ""pathMappings"": [ - { - ""from"": ""F:\\Ruleset.ruleset"", - ""to"": """ + ruleSet.Path.Replace("\", "\\") + """ - }] - }") - - Dim compilation = Await project.GetCompilationAsync() - Assert.Equal(ReportDiagnostic.Warn, compilation.Options.SpecificDiagnosticOptions("CA1001")) - End Using - End Function - - - Public Async Function TestSourceGeneratorOutputIncludedInCompilation() As Task - Dim sourceGeneratorLocation = GetType(TestSourceGenerator.HelloWorldGenerator).Assembly.Location - - Dim project = Await CompilerInvocation.CreateFromJsonAsync(" - { - ""tool"": ""csc"", - ""arguments"": ""/noconfig /analyzer:\""" + sourceGeneratorLocation.Replace("\", "\\") + "\"" /out:Output.dll"", - ""projectFilePath"": ""F:\\Project.csproj"", - ""sourceRootPath"": ""F:\\"" - }") - - Dim compilation = Await project.GetCompilationAsync() - Dim generatedTrees = compilation.SyntaxTrees - - Assert.Single(generatedTrees, Function(t) t.FilePath.EndsWith(TestSourceGenerator.HelloWorldGenerator.GeneratedEnglishClassName + ".cs")) - Assert.Single(generatedTrees, Function(t) t.FilePath.EndsWith(TestSourceGenerator.HelloWorldGenerator.GeneratedSpanishClassName + ".cs")) - End Function - End Class -End Namespace diff --git a/src/Features/Lsif/GeneratorTest/ProjectStructureTests.vb b/src/Features/Lsif/GeneratorTest/ProjectStructureTests.vb deleted file mode 100644 index 1348efb2fbee..000000000000 --- a/src/Features/Lsif/GeneratorTest/ProjectStructureTests.vb +++ /dev/null @@ -1,85 +0,0 @@ -' Licensed to the .NET Foundation under one or more agreements. -' The .NET Foundation licenses this file to you under the MIT license. -' See the LICENSE file in the project root for more information. - -Imports System.IO -Imports System.Text -Imports Microsoft.CodeAnalysis.Collections -Imports Microsoft.CodeAnalysis.LanguageServer -Imports Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Writing -Imports Microsoft.CodeAnalysis.Test.Utilities -Imports Roslyn.Test.Utilities - -Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests - - Public NotInheritable Class ProjectStructureTests - - Public Async Function ProjectContainsDocuments() As Task - Dim lsif = Await TestLsifOutput.GenerateForWorkspaceAsync( - - - - - - ) - - Dim projectVertex = Assert.Single(lsif.Vertices.OfType(Of Graph.LsifProject)) - Dim documentVertices = lsif.GetLinkedVertices(Of Graph.LsifDocument)(projectVertex, "contains") - - Dim documentA = Assert.Single(documentVertices, Function(d) d.Uri.LocalPath = "Z:\A.cs") - Dim documentB = Assert.Single(documentVertices, Function(d) d.Uri.LocalPath = "Z:\B.cs") - - ' We don't include contents for normal files, just generated ones - Assert.Null(documentA.Contents) - Assert.Null(documentB.Contents) - End Function - - - Public Async Function SourceGeneratedDocumentsIncludeContent() As Task - Dim workspace = EditorTestWorkspace.CreateWorkspace( - - - - , openDocuments:=False, composition:=TestLsifOutput.TestComposition) - - workspace.OnAnalyzerReferenceAdded(workspace.CurrentSolution.ProjectIds.Single(), - New TestGeneratorReference(New TestSourceGenerator.HelloWorldGenerator())) - - Dim lsif = Await TestLsifOutput.GenerateForWorkspaceAsync(workspace) - - Dim projectVertex = Assert.Single(lsif.Vertices.OfType(Of Graph.LsifProject)) - Dim generatedDocumentVertices = lsif.GetLinkedVertices(Of Graph.LsifDocument)(projectVertex, "contains") - - For Each generatedDocumentVertex In generatedDocumentVertices - ' Assert the contents were included and does match the tree - Dim contentBase64Encoded = generatedDocumentVertex.Contents - Assert.NotNull(contentBase64Encoded) - - Dim contents = Encoding.UTF8.GetString(Convert.FromBase64String(contentBase64Encoded)) - - Dim compilation = Await workspace.CurrentSolution.Projects.Single().GetCompilationAsync() - Dim tree = Assert.Single(compilation.SyntaxTrees, Function(t) generatedDocumentVertex.Uri.OriginalString.Contains(Path.GetFileName(t.FilePath))) - - Assert.Equal(tree.GetText().ToString(), contents) - Next - End Function - - - Public Async Function SourceGeneratedDocumentHasUriInJson() As Task - Dim workspace = EditorTestWorkspace.CreateWorkspace( - - - - - , openDocuments:=False, composition:=TestLsifOutput.TestComposition) - - Dim stringWriter = New StringWriter - Await TestLsifOutput.GenerateForWorkspaceAsync(workspace, New LineModeLsifJsonWriter(stringWriter)) - - Dim generatedDocument = Assert.Single(Await workspace.CurrentSolution.Projects.Single().GetSourceGeneratedDocumentsAsync()) - Dim uri = SourceGeneratedDocumentUri.Create(generatedDocument.Identity).GetRequiredParsedUri() - Dim outputText = stringWriter.ToString() - Assert.Contains(uri.AbsoluteUri, outputText) - End Function - End Class -End Namespace diff --git a/src/Features/RulesMissingDocumentation.md b/src/Features/RulesMissingDocumentation.md index 5f2d4fc8b93f..1f95fcac254c 100644 --- a/src/Features/RulesMissingDocumentation.md +++ b/src/Features/RulesMissingDocumentation.md @@ -31,6 +31,7 @@ IDE2003 | | Blank line not allowed after constructor initializer colon | IDE2005 | | Blank line not allowed after conditional expression token | IDE2006 | | Blank line not allowed after arrow expression clause token | +IDE3000 | | Implement | JSON001 | | Invalid JSON pattern | JSON002 | | Probable JSON string detected | RE0001 | | Invalid regex pattern | diff --git a/src/Features/Test/CallHierarchy/CallHierarchyServiceTests.cs b/src/Features/Test/CallHierarchy/CallHierarchyServiceTests.cs new file mode 100644 index 000000000000..8fbe1c9eaf50 --- /dev/null +++ b/src/Features/Test/CallHierarchy/CallHierarchyServiceTests.cs @@ -0,0 +1,433 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CallHierarchy; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.CallHierarchy; + +[UseExportProvider] +[Trait(Traits.Feature, Traits.Features.CallHierarchy)] +public sealed class CallHierarchyServiceTests +{ + [Fact] + public async Task CreateItemAsync_ForVirtualMethod_ProvidesExpectedRelationships() + { + using var workspace = TestWorkspace.CreateCSharp(""" + public class Base + { + public virtual void $$M() { } + } + + public class Derived : Base + { + public override void M() { } + } + + class Caller + { + void N(Base b) + { + b.M(); + } + } + """); + + var (_, _, item) = await GetItemAsync(workspace); + + Assert.Equal("M()", item.MemberName); + Assert.Equal("Base", item.ContainingTypeName); + AssertEx.SetEqual( + [ + CallHierarchyRelationshipKind.Callers, + CallHierarchyRelationshipKind.CallsToOverrides, + CallHierarchyRelationshipKind.Overrides, + ], + item.SupportedSearchDescriptors.Select(static d => d.Relationship)); + } + + [Fact] + public async Task SearchIncomingCallsAsync_Callers_RespectsDocumentFilter() + { + using var workspace = TestWorkspace.Create(""" + + + +namespace C +{ + public class CC + { + public int GetFive() { return 5; } + } +} + + +using C; +namespace G +{ + public class G + { + public void G() + { + CC c = new CC(); + c.GetFive(); + } + } +} + + + + Assembly1 + +using C; +public class D +{ + void bar() + { + var c = new C.CC(); + var d = c.Ge$$tFive(); + } +} + + +using C; +public class DSSS +{ + void bar() + { + var c = new C.CC(); + var d = c.GetFive(); + } +} + + + +"""); + + var filteredDocument = workspace.CurrentSolution.GetRequiredDocument(workspace.Documents.Single(d => d.Name == "Test3.cs").Id); + var results = await GetSearchResultsAsync( + workspace, + CallHierarchyRelationshipKind.Callers, + documents: ImmutableHashSet.Create(filteredDocument)); + + AssertEx.SetEqual(["D.bar()"], GetItemDisplayNames(results)); + } + + [Fact] + public async Task SearchIncomingCallsAsync_Implementations_FindsCrossProjectImplementations() + { + using var workspace = TestWorkspace.Create(""" + + + +namespace C +{ + public interface I + { + void go$$o(); + } + + public class C : I + { + public void goo() { } + } +} + + +using C; +namespace G +{ + public class G : I + { + public void goo() + { + } + } +} + + + + Assembly1 + +using C; +public class D : I +{ + public void goo() + { + } +} + + + +"""); + + var results = await GetSearchResultsAsync(workspace, CallHierarchyRelationshipKind.Implementations); + + AssertEx.SetEqual(["C.goo()", "D.goo()", "G.goo()"], GetItemDisplayNames(results)); + } + + [Fact] + public async Task SearchIncomingCallsAsync_FieldReferences_ProducesInitializerResult() + { + using var workspace = TestWorkspace.CreateCSharp(""" + class C + { + int $$f = 0; + int g = f; + int h = f + 1; + + void M() + { + var value = f; + } + } + """); + + var results = await GetSearchResultsAsync(workspace, CallHierarchyRelationshipKind.FieldReferences); + + AssertEx.SetEqual(["C.M()"], GetItemDisplayNames(results)); + + var locationResult = Assert.Single(results.Where(r => r.Item is null)); + Assert.Equal(2, locationResult.ReferenceLocations.Length); + } + + [Fact] + public async Task SearchOutgoingCallsAsync_ReturnsDirectTargetsFromSelectedDocument() + { + using var workspace = TestWorkspace.Create(""" + + + +class C +{ + void $$M() + { + N(); + var value = P; + } + + void N() + { + } + + int P => 1; +} + + +class D +{ + void M() + { + var c = new C(); + c.M(); + } +} + + + +"""); + + var (document, service, item) = await GetItemAsync(workspace); + var results = await service.SearchOutgoingCallsAsync( + document.Project.Solution, + item.ItemId, + ImmutableHashSet.Create(document), + CancellationToken.None); + + AssertEx.SetEqual(["C.N()", "C.P"], GetItemDisplayNames(results.Cast().ToImmutableArray())); + Assert.All(results, static result => Assert.Single(result.ReferenceLocations)); + } + + [Fact] + public async Task SearchOutgoingCallsAsync_IncludesImplicitConstructors() + { + using var workspace = TestWorkspace.CreateCSharp(""" + class C + { + } + + class Caller + { + void $$M() + { + var c = new C(); + } + } + """); + + var (document, service, item) = await GetItemAsync(workspace); + var results = await service.SearchOutgoingCallsAsync( + document.Project.Solution, + item.ItemId, + ImmutableHashSet.Create(document), + CancellationToken.None); + + var constructorCall = Assert.Single(results); + Assert.NotNull(constructorCall.Item); + Assert.Equal("C()", constructorCall.Item.MemberName); + Assert.Equal("C", constructorCall.Item.ContainingTypeName); + Assert.Single(constructorCall.ReferenceLocations); + } + + [Fact] + public async Task SearchOutgoingCallsAsync_RespectsDocumentFilter() + { + using var workspace = TestWorkspace.Create(""" + + + +class C +{ + void $$M() + { + N(); + var value = P; + } + + void N() + { + } + + int P => 1; +} + + +class D +{ + void M() + { + var c = new C(); + c.M(); + } +} + + + +"""); + + var declarationDocument = workspace.CurrentSolution.GetRequiredDocument(workspace.Documents.Single(d => d.Name == "Test1.cs").Id); + var otherDocument = workspace.CurrentSolution.GetRequiredDocument(workspace.Documents.Single(d => d.Name == "Test2.cs").Id); + + var resultsFromDeclaration = await GetOutgoingSearchResultsAsync( + workspace, + documents: ImmutableHashSet.Create(declarationDocument)); + var resultsFromOtherDocument = await GetOutgoingSearchResultsAsync( + workspace, + documents: ImmutableHashSet.Create(otherDocument)); + + AssertEx.SetEqual(["C.N()", "C.P"], GetItemDisplayNames(resultsFromDeclaration)); + Assert.Empty(resultsFromOtherDocument); + } + + [Fact] + public async Task SearchOutgoingCallsAsync_FindsCrossProjectTargets() + { + using var workspace = TestWorkspace.Create(""" + + + +namespace C +{ + public class CC + { + public int GetFive() { return 5; } + } +} + + + + Assembly1 + +using C; +public class D +{ + public int $$M() + { + var c = new CC(); + return c.GetFive(); + } +} + + + +"""); + + var results = await GetOutgoingSearchResultsAsync(workspace); + + AssertEx.SetEqual(["CC.CC()", "CC.GetFive()"], GetItemDisplayNames(results)); + } + + [Fact] + public async Task SearchOutgoingCallsAsync_FieldInitializer_ProducesResult() + { + using var workspace = TestWorkspace.CreateCSharp(""" + class C + { + int $$f = GetValue(); + + static int GetValue() + { + return 0; + } + } + """); + + var results = await GetOutgoingSearchResultsAsync(workspace); + + var call = Assert.Single(results); + Assert.NotNull(call.Item); + Assert.Equal("GetValue()", call.Item.MemberName); + Assert.Equal("C", call.Item.ContainingTypeName); + Assert.Single(call.ReferenceLocations); + } + + private static IEnumerable GetItemDisplayNames(ImmutableArray results) + => results.Where(static r => r.Item is not null).Select(static r => GetDisplayName(r.Item!)); + + private static string GetDisplayName(CallHierarchyItemDescriptor item) + => string.IsNullOrEmpty(item.ContainingTypeName) + ? item.MemberName + : $"{item.ContainingTypeName}.{item.MemberName}"; + + private static async Task> GetSearchResultsAsync( + TestWorkspace workspace, + CallHierarchyRelationshipKind relationship, + IImmutableSet? documents = null) + { + var (document, service, item) = await GetItemAsync(workspace); + var searchDescriptor = Assert.Single(item.SupportedSearchDescriptors.Where(d => d.Relationship == relationship)); + return await service.SearchIncomingCallsAsync(document.Project.Solution, searchDescriptor, documents, CancellationToken.None); + } + + private static async Task> GetOutgoingSearchResultsAsync( + TestWorkspace workspace, + IImmutableSet? documents = null) + { + var (document, service, item) = await GetItemAsync(workspace); + return await service.SearchOutgoingCallsAsync(document.Project.Solution, item.ItemId, documents, CancellationToken.None); + } + + private static async Task<(Document Document, ICallHierarchyService Service, CallHierarchyItemDescriptor Item)> GetItemAsync(TestWorkspace workspace) + { + var hostDocument = workspace.DocumentWithCursor; + var document = workspace.CurrentSolution.GetRequiredDocument(hostDocument.Id); + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, hostDocument.CursorPosition!.Value, cancellationToken: CancellationToken.None); + Assert.NotNull(symbol); + + var service = document.GetRequiredLanguageService(); + var item = await service.CreateItemAsync(symbol, document.Project, CancellationToken.None); + Assert.NotNull(item); + + return (document, service, item); + } +} diff --git a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs index 123eac2afa0c..d28d58322bdd 100644 --- a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs +++ b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs @@ -232,8 +232,25 @@ public async Task DifferentDocumentWithSameContent() public async Task ProjectThatDoesNotSupportEnC_Language(bool breakMode) { using var _ = CreateWorkspace(out var solution, out var service, [typeof(NoCompilationLanguageService)]); - var project = solution.AddProject("dummy_proj", "dummy_proj", NoCompilationConstants.LanguageName); - var document = project.AddDocument("test", "dummy1"); + + var projectId = ProjectId.CreateNewId("dummy_proj"); + + var sourceFilePath = Path.Combine(TempRoot.Root, "test"); + var source = CreateText("class C;"); + + // Use C# to compile the F# library. The metadata doesn't matter for this test, only the document debug info. + LoadLibraryToDebuggee(EmitLibrary(projectId, [(source, sourceFilePath)], assemblyName: "dummy_proj")); + + var project = solution.AddProject(ProjectInfo.Create( + projectId, + VersionStamp.Create(), + name: "dummy_proj", + assemblyName: "dummy_proj", + language: NoCompilationConstants.LanguageName, + filePath: Path.Combine(TempRoot.Root, "dummy.proj")) + .WithCompilationOutputInfo(new CompilationOutputInfo())).GetRequiredProject(projectId); + + var document = project.AddDocument(name: "test", source, filePath: sourceFilePath); solution = document.Project.Solution; var debuggingSession = StartDebuggingSession(service, solution); @@ -252,9 +269,19 @@ public async Task ProjectThatDoesNotSupportEnC_Language(bool breakMode) // validate solution update status and emit: var results = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); - Assert.Empty(results.Diagnostics); + + var message = string.Format( + FeaturesResources.Changing_source_file_0_in_a_project_1_that_does_not_support_hot_reload_requires_restarting_the_application_2, + ["test", "dummy_proj", string.Format(FeaturesResources._0_does_not_support_Hot_Reload, NoCompilationConstants.LanguageName)]); + + AssertEx.Equal( + [ + $"dummy_proj: {document.FilePath}: (0,0)-(0,0): Error ENC1009: {message}" + ], InspectDiagnostics(results.Diagnostics)); + + AssertEx.SequenceEqual([projectId], results.ProjectsToRestart.Keys); var document2 = solution.GetDocument(document1.Id); diagnostics = await service.GetDocumentDiagnosticsAsync(document2, s_noActiveSpans, CancellationToken.None); @@ -302,6 +329,16 @@ public void F() {} [ $"test: {sourceFile.Path}: (2,0)-(2,22): Error ENC2012: {string.Format(FeaturesResources.EditAndContinueDisallowedByProject, ["test", "*optimized*"])}" ], InspectDiagnostics(results.Diagnostics)); + + debuggingSession.DiscardSolutionUpdate(); + EndDebuggingSession(debuggingSession); + + AssertEx.SequenceEqual( + [ + "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=0|HotReloadSessionCount=1|EmptyHotReloadSessionCount=0", + "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=True|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=1|InBreakState=False|Capabilities=31|ProjectIdsWithAppliedChanges=|ProjectIdsWithUpdatedBaselines=", + $"Debugging_EncSession_EditSession_EmitDeltaErrorId: SessionId=1|EditSessionId=2|ErrorId=ENC2012" + ], _telemetryLog); } [Fact] @@ -425,6 +462,13 @@ End Class debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); + + AssertEx.SequenceEqual( + [ + "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=0|HotReloadSessionCount=1|EmptyHotReloadSessionCount=0", + "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=1|InBreakState=False|Capabilities=31|ProjectIdsWithAppliedChanges=|ProjectIdsWithUpdatedBaselines=", + $"Debugging_EncSession_EditSession_EmitDeltaErrorId: SessionId=1|EditSessionId=2|ErrorId={code}" + ], _telemetryLog); } [Theory] @@ -513,6 +557,23 @@ End Class } EndDebuggingSession(debuggingSession); + + if (isWarning) + { + AssertEx.SequenceEqual( + [ + "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=0|HotReloadSessionCount=0|EmptyHotReloadSessionCount=1", + ], _telemetryLog); + } + else + { + AssertEx.SequenceEqual( + [ + "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=0|HotReloadSessionCount=1|EmptyHotReloadSessionCount=0", + "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=1|InBreakState=False|Capabilities=31|ProjectIdsWithAppliedChanges=|ProjectIdsWithUpdatedBaselines=", + $"Debugging_EncSession_EditSession_EmitDeltaErrorId: SessionId=1|EditSessionId=2|ErrorId={code}" + ], _telemetryLog); + } } [Theory] @@ -585,6 +646,13 @@ End Class debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); + + AssertEx.SequenceEqual( + [ + "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=0|HotReloadSessionCount=1|EmptyHotReloadSessionCount=0", + "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=1|InBreakState=False|Capabilities=31|ProjectIdsWithAppliedChanges=|ProjectIdsWithUpdatedBaselines=", + $"Debugging_EncSession_EditSession_EmitDeltaErrorId: SessionId=1|EditSessionId=2|ErrorId={code}" + ], _telemetryLog); } [Fact] @@ -1122,16 +1190,18 @@ public async Task ErrorReadingPdbFile() var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); - AssertEx.Equal( + AssertEx.SequenceEqual( [$"proj: {document2.FilePath}: (0,0)-(0,0): Error ENC1006: {string.Format(FeaturesResources.UnableToReadSourceFileOrPdb, sourceFile.Path)}"], InspectDiagnostics(results.Diagnostics)); debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); - AssertEx.Equal( + AssertEx.SequenceEqual( [ - "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=1|HotReloadSessionCount=0|EmptyHotReloadSessionCount=1" + "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=1|EmptySessionCount=0|HotReloadSessionCount=0|EmptyHotReloadSessionCount=1", + "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=False|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=0|EmitDeltaErrorIdCount=1|InBreakState=True|Capabilities=31|ProjectIdsWithAppliedChanges=|ProjectIdsWithUpdatedBaselines=", + "Debugging_EncSession_EditSession_EmitDeltaErrorId: SessionId=1|EditSessionId=2|ErrorId=ENC1006" ], _telemetryLog); } @@ -2277,10 +2347,10 @@ public async Task HasChanges() var projectE = solution.AddTestProject("E", language: NoCompilationConstants.LanguageName); solution = projectE.Solution; - Assert.False(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None)); + Assert.True(await EditSession.HasChangesAsync(oldSolution, solution, CancellationToken.None)); // remove a project that doesn't support EnC: - Assert.False(await EditSession.HasChangesAsync(solution, oldSolution, CancellationToken.None)); + Assert.True(await EditSession.HasChangesAsync(solution, oldSolution, CancellationToken.None)); EndDebuggingSession(debuggingSession); } @@ -3051,27 +3121,43 @@ public async Task ValidSignificantChange_ApplyBeforeFileWatcherEvent(bool saveDo // Scenarios tested: // // SaveDocument=true - // workspace: --V0-------------|--V2--------|------------| - // file system: --V0---------V1--|-----V2-----|------------| - // \--build--/ F5 ^ F10 ^ F10 - // save file watcher: no-op - // SaveDocument=false - // workspace: --V0-------------|--V2--------|----V1------| - // file system: --V0---------V1--|------------|------------| - // \--build--/ F5 F10 ^ F10 - // file watcher: workspace update + // opened doc is updated + // v + // workspace: --V0-------------|--------------V2--------|------------| + // file system: --V0---------V1--|-----------------V2-----|------------| + // text provider: -----------------|-----V1-----------------|------------| + // \--build--/ F5 ^ ^ F10 ^ F10 + // open save file watcher: no-op + // + // Text provider captures V1 snapshot when doc is open. Without it we wouldn't have source text matching the PDB when apply is triggered (F10), + // since both workspace and file system are already on V2. + // + // SaveDocument=false + // workspace: --V0-------------|--------------V2--------|----V1------| + // file system: --V0---------V1--|------------------------|------------| + // \--build--/ F5 F10 ^ F10 + // file watcher: workspace update - var source1 = "class C1 { void M() { System.Console.WriteLine(1); } }"; + var checksumAlg = SourceHashAlgorithms.Default; + var sjis = Encoding.GetEncoding("SJIS"); + + var source0 = "class C1 { void こんにちは() { System.Console.WriteLine(0); } }"; + var source1 = "class C1 { void こんにちは() { System.Console.WriteLine(1); } }"; + var source2 = "class C1 { void こんにちは() { System.Console.WriteLine(2); } }"; + + var sourceText0 = CreateText(source0, sjis, checksumAlg); + var sourceText1 = CreateText(source1, sjis, checksumAlg); + var sourceText2 = CreateText(source2, sjis, checksumAlg); var dir = Temp.CreateDirectory(); - var sourceFile = dir.CreateFile("test.cs").WriteAllText(source1, Encoding.UTF8); + var sourceFile = dir.CreateFile("test.cs").WriteAllText(source1, sjis); using var _ = CreateWorkspace(out var solution, out var service); // the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build): var document1 = solution. - AddTestProject("test"). - AddDocument("test.cs", CreateText("class C1 { void M() { System.Console.WriteLine(0); } }"), filePath: sourceFile.Path); + AddTestProject("test", out var projectId). + AddDocument("test.cs", sourceText1, filePath: sourceFile.Path); var documentId = document1.Id; solution = document1.Project.Solution; @@ -3081,33 +3167,36 @@ public async Task ValidSignificantChange_ApplyBeforeFileWatcherEvent(bool saveDo TryGetMatchingSourceTextImpl = (filePath, requiredChecksum, checksumAlgorithm) => { Assert.Equal(sourceFile.Path, filePath); - AssertEx.Equal(requiredChecksum, CreateText(source1).GetChecksum()); - Assert.Equal(SourceHashAlgorithms.Default, checksumAlgorithm); - + AssertEx.Equal(requiredChecksum, sourceText1.GetChecksum()); + Assert.Equal(checksumAlg, checksumAlgorithm); return source1; } }; - var moduleId = EmitAndLoadLibraryToDebuggee(documentId.ProjectId, source1, sourceFilePath: sourceFile.Path); + var moduleId = EmitAndLoadLibraryToDebuggee(documentId.ProjectId, source1, encoding: sjis, checksumAlgorithm: checksumAlg, sourceFilePath: sourceFile.Path); var debuggingSession = StartDebuggingSession(service, solution, initialState: CommittedSolution.DocumentState.None, sourceTextProvider); EnterBreakState(debuggingSession); // The user opens the source file and changes the source before Roslyn receives file watcher event. - var source2 = "class C1 { void M() { System.Console.WriteLine(2); } }"; - solution = solution.WithDocumentText(documentId, CreateText(source2)); - var document2 = solution.GetDocument(documentId); + solution = solution.WithDocumentText(documentId, sourceText2); // Save the document: if (saveDocument) { - sourceFile.WriteAllText(source2, Encoding.UTF8); + sourceFile.WriteAllText(source2, sjis); } // EnC service queries for a document, which triggers read of the source file from disk. var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Empty(results.Diagnostics); + Assert.Null(results.SyntaxError); + + // committed doc content has to match to the version used by the compiler, encoding and checksum do not: + var committedDocument1 = debuggingSession.LastCommittedSolution.GetRequiredProject(projectId).GetRequiredDocument(documentId); + var committedDocumentText1 = await committedDocument1.GetTextAsync(CancellationToken.None); + Assert.Equal(sourceText1.ToString(), committedDocumentText1.ToString()); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); @@ -3117,11 +3206,16 @@ public async Task ValidSignificantChange_ApplyBeforeFileWatcherEvent(bool saveDo EnterBreakState(debuggingSession); // file watcher updates the workspace: - solution = solution.WithDocumentText(documentId, CreateTextFromFile(sourceFile.Path)); - var document3 = solution.Projects.Single().Documents.Single(); + solution = solution.WithDocumentText(documentId, CreateTextFromFile(sourceFile.Path, sjis)); results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Empty(results.Diagnostics); + Assert.Null(results.SyntaxError); + + // committed doc content has to match content when CommitSolutionUpdate was called, encoding and checksum do not: + var committedDocument2 = debuggingSession.LastCommittedSolution.GetRequiredProject(projectId).GetRequiredDocument(documentId); + var committedDocumentText2 = await committedDocument2.GetTextAsync(CancellationToken.None); + Assert.Equal(sourceText2.ToString(), committedDocumentText2.ToString()); if (saveDocument) { diff --git a/src/Features/Test/Microsoft.CodeAnalysis.Features.UnitTests.csproj b/src/Features/Test/Microsoft.CodeAnalysis.Features.UnitTests.csproj index fec481ae6128..cf7bdf0fde2a 100644 --- a/src/Features/Test/Microsoft.CodeAnalysis.Features.UnitTests.csproj +++ b/src/Features/Test/Microsoft.CodeAnalysis.Features.UnitTests.csproj @@ -5,7 +5,7 @@ Library Microsoft.CodeAnalysis.UnitTests - $(NetVSShared);net472 + $(NetRoslynWindowsTests);net472 true diff --git a/src/Features/Test/NavigateTo/RegexDetectionTests.cs b/src/Features/Test/NavigateTo/RegexDetectionTests.cs new file mode 100644 index 000000000000..9326b3807b4e --- /dev/null +++ b/src/Features/Test/NavigateTo/RegexDetectionTests.cs @@ -0,0 +1,247 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis.EmbeddedLanguages.RegularExpressions; +using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars; +using Microsoft.CodeAnalysis.NavigateTo; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.NavigateTo; + +public sealed class RegexDetectionTests +{ + private static (string? container, string name) SplitOnContainerDot(string pattern) + { + var sequence = VirtualCharSequence.Create(0, pattern); + var tree = RegexParser.TryParse(sequence, RegexOptions.None); + if (tree is not { Diagnostics: [] }) + return (null, pattern); + + return RegexPatternDetector.SplitOnContainerDot(pattern, tree); + } + + #region IsRegexPattern — positive (is regex) + + [Theory] + [InlineData("(Read|Write)")] + [InlineData("Read|Write")] + [InlineData("[abc]")] + [InlineData("Goo.*Bar")] + [InlineData("Goo.+Bar")] + [InlineData("x+")] + [InlineData("x?")] + [InlineData("x*")] + [InlineData(@"a\d")] + [InlineData("^Start")] + [InlineData("End$")] + [InlineData("a{2,3}")] + [InlineData("a{2}")] + [InlineData(@"Goo\.Bar")] + [InlineData("(Read|Write)Line")] + [InlineData("(?:abc)")] + public void IsRegexPattern_Positive(string pattern) + { + Assert.True(RegexPatternDetector.IsRegexPattern(pattern)); + } + + #endregion + + #region IsRegexPattern — negative (not regex) + + [Theory] + [InlineData("abc")] + [InlineData("GooBar")] + [InlineData("x.y")] + [InlineData("Goo.Bar.Baz")] + [InlineData("get word")] + [InlineData("ReadLine")] + [InlineData("_myField")] + [InlineData("System.IO.File")] + [InlineData("")] + [InlineData("a")] + public void IsRegexPattern_Negative(string pattern) + { + Assert.False(RegexPatternDetector.IsRegexPattern(pattern)); + } + + #endregion + + #region SplitOnContainerDot — positive (split occurs) + + [Fact] + public void Split_PlainGooDotBar() + { + // Goo.Bar -> bare dot splits into container="Goo", name="Bar" + var (container, name) = SplitOnContainerDot("Goo.Bar"); + Assert.Equal("Goo", container); + Assert.Equal("Bar", name); + } + + [Fact] + public void Split_RegexContainerPlainName() + { + // (Goo|Bar).Baz -> bare dot after ')' -> container="(Goo|Bar)", name="Baz" + var (container, name) = SplitOnContainerDot("(Goo|Bar).Baz"); + Assert.Equal("(Goo|Bar)", container); + Assert.Equal("Baz", name); + } + + [Fact] + public void Split_RegexContainerRegexName() + { + // (Goo|Bar).(Baz|Quux) -> bare dot -> container="(Goo|Bar)", name="(Baz|Quux)" + var (container, name) = SplitOnContainerDot("(Goo|Bar).(Baz|Quux)"); + Assert.Equal("(Goo|Bar)", container); + Assert.Equal("(Baz|Quux)", name); + } + + [Fact] + public void Split_MultipleDots_SplitsOnLast() + { + // System.IO.File -> last bare dot is before "File" + var (container, name) = SplitOnContainerDot("System.IO.File"); + Assert.Equal("System.IO", container); + Assert.Equal("File", name); + } + + [Fact] + public void Split_RegexContainerWithMultipleDots() + { + // System.(IO|Net).File -> last bare dot is before "File" + var (container, name) = SplitOnContainerDot("System.(IO|Net).File"); + Assert.Equal("System.(IO|Net)", container); + Assert.Equal("File", name); + } + + [Fact] + public void Split_ReadDotLine() + { + // Read.Line -> bare dot splits + var (container, name) = SplitOnContainerDot("Read.Line"); + Assert.Equal("Read", container); + Assert.Equal("Line", name); + } + + #endregion + + #region SplitOnContainerDot — negative (no split) + + [Fact] + public void NoSplit_GooDotStarBar() + { + // Goo.*Bar -> the dot is quantified with *, not bare -> no split + var (container, name) = SplitOnContainerDot("Goo.*Bar"); + Assert.Null(container); + Assert.Equal("Goo.*Bar", name); + } + + [Fact] + public void NoSplit_GooDotPlusBar() + { + // Goo.+Bar -> the dot is quantified with +, not bare -> no split + var (container, name) = SplitOnContainerDot("Goo.+Bar"); + Assert.Null(container); + Assert.Equal("Goo.+Bar", name); + } + + [Fact] + public void NoSplit_EscapedDot() + { + // Goo\.Bar -> escape node, not a wildcard -> no split + var (container, name) = SplitOnContainerDot(@"Goo\.Bar"); + Assert.Null(container); + Assert.Equal(@"Goo\.Bar", name); + } + + [Fact] + public void NoSplit_NoDotAtAll() + { + // (Read|Write)Line -> no dot at all + var (container, name) = SplitOnContainerDot("(Read|Write)Line"); + Assert.Null(container); + Assert.Equal("(Read|Write)Line", name); + } + + [Fact] + public void NoSplit_PlainTextNoDot() + { + var (container, name) = SplitOnContainerDot("ReadLine"); + Assert.Null(container); + Assert.Equal("ReadLine", name); + } + + [Fact] + public void NoSplit_TopLevelAlternation() + { + // Goo.Bar|Baz.Quux -> top-level alternation has two branches; dot is inside a branch, not top-level + var (container, name) = SplitOnContainerDot("Goo.Bar|Baz.Quux"); + Assert.Null(container); + Assert.Equal("Goo.Bar|Baz.Quux", name); + } + + [Fact] + public void NoSplit_DotQuestionBar() + { + // Goo.?Bar -> the dot is quantified with ? -> no split + var (container, name) = SplitOnContainerDot("Goo.?Bar"); + Assert.Null(container); + Assert.Equal("Goo.?Bar", name); + } + + [Fact] + public void NoSplit_InvalidRegex() + { + // Unbalanced parens -> parser returns diagnostics -> no split, return as-is + var (container, name) = SplitOnContainerDot("(Goo.Bar"); + Assert.Null(container); + Assert.Equal("(Goo.Bar", name); + } + + [Fact] + public void NoSplit_EmptyPattern() + { + var (container, name) = SplitOnContainerDot(""); + Assert.Null(container); + Assert.Equal("", name); + } + + [Fact] + public void NoSplit_DotAtStart() + { + // .Goo -> the bare dot is at position 0, so containerEnd == 0 -> skip it + var (container, name) = SplitOnContainerDot(".Goo"); + Assert.Null(container); + Assert.Equal(".Goo", name); + } + + [Fact] + public void NoSplit_DotAtEnd() + { + // Goo. -> the bare dot is at the last position, so nameStart >= pattern.Length -> skip it + var (container, name) = SplitOnContainerDot("Goo."); + Assert.Null(container); + Assert.Equal("Goo.", name); + } + + [Fact] + public void Split_DotAtStartAndMiddle_SplitsOnMiddle() + { + // .Goo.Bar -> leading dot is skipped (containerEnd == 0), middle dot splits + var (container, name) = SplitOnContainerDot(".Goo.Bar"); + Assert.Equal(".Goo", container); + Assert.Equal("Bar", name); + } + + [Fact] + public void Split_DotAtMiddleAndEnd_SplitsOnMiddle() + { + // Goo.Bar. -> trailing dot is skipped (nameStart >= Length), middle dot splits + var (container, name) = SplitOnContainerDot("Goo.Bar."); + Assert.Equal("Goo", container); + Assert.Equal("Bar.", name); + } + + #endregion +} diff --git a/src/Features/Test/NavigateTo/RegexQueryCompilerTests.cs b/src/Features/Test/NavigateTo/RegexQueryCompilerTests.cs new file mode 100644 index 000000000000..fdbda82a044e --- /dev/null +++ b/src/Features/Test/NavigateTo/RegexQueryCompilerTests.cs @@ -0,0 +1,392 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using Microsoft.CodeAnalysis.NavigateTo; +using Microsoft.CodeAnalysis.PatternMatching; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.NavigateTo; + +public sealed class RegexQueryCompilerTests +{ + #region Compilation — positive (produces Literal nodes) + + [Fact] + public void PlainText_ProducesLiteral() + { + var query = RegexQueryCompiler.Compile("ReadLine"); + var literal = Assert.IsType(query); + Assert.Equal("readline", literal.Text); + } + + [Fact] + public void Alternation_ProducesAny() + { + // (Read|Write)Line -> All(Any(Literal("read"), Literal("write")), Literal("line")) + var query = RegexQueryCompiler.Compile("(Read|Write)Line"); + var all = Assert.IsType(query); + Assert.Equal(2, all.Children.Length); + + var any = Assert.IsType(all.Children[0]); + Assert.Equal(2, any.Children.Length); + Assert.Equal("read", Assert.IsType(any.Children[0]).Text); + Assert.Equal("write", Assert.IsType(any.Children[1]).Text); + + Assert.Equal("line", Assert.IsType(all.Children[1]).Text); + } + + [Fact] + public void Sequence_ProducesAll() + { + // Goo.*Bar -> All(Literal("goo"), Literal("bar")) (the .* becomes None, pruned by optimizer) + var query = RegexQueryCompiler.Compile("Goo.*Bar"); + var all = Assert.IsType(query); + Assert.Equal(2, all.Children.Length); + Assert.Equal("goo", Assert.IsType(all.Children[0]).Text); + Assert.Equal("bar", Assert.IsType(all.Children[1]).Text); + } + + [Fact] + public void OneOrMore_PreservesInner() + { + // Goo+ -> the parser sees "Go" as text, then o+ as OneOrMore(text("o")) + var query = RegexQueryCompiler.Compile("Goo+"); + Assert.True(query!.HasLiterals); + } + + [Fact] + public void EscapedDot_ProducesLiteral() + { + // Goo\.Bar -> the escaped dot is a literal '.' + var query = RegexQueryCompiler.Compile(@"Goo\.Bar"); + Assert.NotNull(query); + Assert.True(query!.HasLiterals); + } + + [Fact] + public void EscapedBracket_ProducesLiteral() + { + // \[Test\] -> literals '[', 'T', 'e', 's', 't', ']' + var query = RegexQueryCompiler.Compile(@"\[Test\]"); + Assert.NotNull(query); + Assert.True(query!.HasLiterals); + } + + [Fact] + public void NonCapturingGroup_RecursesInto() + { + // (?:Read|Write) -> Any(Literal("read"), Literal("write")) + var query = RegexQueryCompiler.Compile("(?:Read|Write)"); + var any = Assert.IsType(query); + Assert.Equal(2, any.Children.Length); + Assert.Equal("read", Assert.IsType(any.Children[0]).Text); + Assert.Equal("write", Assert.IsType(any.Children[1]).Text); + } + + [Fact] + public void ExactNumericQuantifier_WithMinOne_PreservesInner() + { + // (Go){3} -> the group "Go" produces Literal("go"), and exact count >= 1 preserves it + var query = RegexQueryCompiler.Compile("(Go){3}"); + Assert.True(query!.HasLiterals); + Assert.Equal("go", Assert.IsType(query).Text); + } + + [Fact] + public void OpenRangeQuantifier_WithMinOne_PreservesInner() + { + // (Go){1,} -> Literal("go") + var query = RegexQueryCompiler.Compile("(Go){1,}"); + Assert.True(query!.HasLiterals); + Assert.Equal("go", Assert.IsType(query).Text); + } + + [Fact] + public void ClosedRangeQuantifier_WithMinOne_PreservesInner() + { + // (Go){1,5} -> Literal("go") + var query = RegexQueryCompiler.Compile("(Go){1,5}"); + Assert.True(query!.HasLiterals); + Assert.Equal("go", Assert.IsType(query).Text); + } + + #endregion + + #region Compilation — negative (returns null: no extractable literals) + + [Fact] + public void DotStar_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile(".*")); + + [Fact] + public void SingleDot_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile(".")); + + [Fact] + public void CharacterClassEscape_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile(@"\d+")); + + [Fact] + public void CharacterClass_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("[abc]")); + + [Fact] + public void ZeroOrMore_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("a*")); + + [Fact] + public void ZeroOrOne_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("a?")); + + [Fact] + public void ZeroOrMoreLazy_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("a*?")); + + [Fact] + public void NumericQuantifier_WithMinZero_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("a{0,5}")); + + [Fact] + public void OpenRangeQuantifier_WithMinZero_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("a{0,}")); + + [Fact] + public void InvalidRegex_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("(abc")); + + [Fact] + public void AnchorOnly_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("^$")); + + [Fact] + public void WhitespaceOnlyText_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("^ $")); + + #endregion + + #region Optimization + + [Fact] + public void Optimizer_FlattenNestedAll() + { + var inner = new RegexQuery.All([new RegexQuery.Literal("aa"), new RegexQuery.Literal("bb")]); + var outer = new RegexQuery.All([inner, new RegexQuery.Literal("cc")]); + + var optimized = RegexQuery.Optimize(outer); + var all = Assert.IsType(optimized); + Assert.Equal(3, all.Children.Length); + Assert.Equal("aa", Assert.IsType(all.Children[0]).Text); + Assert.Equal("bb", Assert.IsType(all.Children[1]).Text); + Assert.Equal("cc", Assert.IsType(all.Children[2]).Text); + } + + [Fact] + public void Optimizer_FlattenNestedAny() + { + var inner = new RegexQuery.Any([new RegexQuery.Literal("aa"), new RegexQuery.Literal("bb")]); + var outer = new RegexQuery.Any([inner, new RegexQuery.Literal("cc")]); + + var optimized = RegexQuery.Optimize(outer); + var any = Assert.IsType(optimized); + Assert.Equal(3, any.Children.Length); + } + + [Fact] + public void Optimizer_PruneNoneFromAll() + { + var query = new RegexQuery.All([ + new RegexQuery.Literal("aa"), + RegexQuery.None.Instance, + new RegexQuery.Literal("bb"), + ]); + + var optimized = RegexQuery.Optimize(query); + var all = Assert.IsType(optimized); + Assert.Equal(2, all.Children.Length); + Assert.Equal("aa", Assert.IsType(all.Children[0]).Text); + Assert.Equal("bb", Assert.IsType(all.Children[1]).Text); + } + + [Fact] + public void Optimizer_NoneInAny_CollapsesToNone() + { + var query = new RegexQuery.Any([ + new RegexQuery.Literal("aa"), + RegexQuery.None.Instance, + ]); + + var optimized = RegexQuery.Optimize(query); + Assert.IsType(optimized); + } + + [Fact] + public void Optimizer_SingleChildAll_Unwraps() + { + var query = new RegexQuery.All([new RegexQuery.Literal("aa")]); + var optimized = RegexQuery.Optimize(query); + Assert.Equal("aa", Assert.IsType(optimized).Text); + } + + [Fact] + public void Optimizer_SingleChildAny_Unwraps() + { + var query = new RegexQuery.Any([new RegexQuery.Literal("aa")]); + var optimized = RegexQuery.Optimize(query); + Assert.Equal("aa", Assert.IsType(optimized).Text); + } + + [Fact] + public void Optimizer_AllNone_CollapsesToNone() + { + var query = new RegexQuery.All([RegexQuery.None.Instance, RegexQuery.None.Instance]); + var optimized = RegexQuery.Optimize(query); + Assert.IsType(optimized); + } + + #endregion + + #region Single-char literals are too short for pre-filtering + + [Fact] + public void SingleCharLiteral_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("a")); + + [Fact] + public void SingleCharAlternation_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("(a|b)")); + + [Fact] + public void SingleCharWithWildcard_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("a.b")); + + #endregion + + #region HasLiterals + + [Fact] + public void HasLiterals_True_ForLiteral() + { + Assert.True(new RegexQuery.Literal("goo").HasLiterals); + } + + [Fact] + public void HasLiterals_False_ForNone() + { + Assert.False(RegexQuery.None.Instance.HasLiterals); + } + + [Fact] + public void HasLiterals_True_ForAllWithLiteral() + { + var query = new RegexQuery.All([new RegexQuery.Literal("goo"), RegexQuery.None.Instance]); + Assert.True(query.HasLiterals); + } + + [Fact] + public void HasLiterals_True_ForAnyWithLiteral() + { + var query = new RegexQuery.Any([new RegexQuery.Literal("goo"), new RegexQuery.Literal("bar")]); + Assert.True(query.HasLiterals); + } + + [Fact] + public void HasLiterals_False_ForAllOfNone() + { + var query = new RegexQuery.All([RegexQuery.None.Instance]); + Assert.False(query.HasLiterals); + } + + #endregion + + #region End-to-end compilation + optimization + + [Fact] + public void EndToEnd_AlternationWithSharedSuffix() + { + var query = RegexQueryCompiler.Compile("(Read|Write)Line")!; + Assert.True(query.HasLiterals); + + var all = Assert.IsType(query); + Assert.Equal(2, all.Children.Length); + Assert.IsType(all.Children[0]); + Assert.IsType(all.Children[1]); + } + + [Fact] + public void EndToEnd_OptionalSuffix() + { + // Read(Line)? -> "read" is required, "(Line)?" is optional (None) + // After optimization: Literal("read") + var query = RegexQueryCompiler.Compile("Read(Line)?")!; + Assert.True(query.HasLiterals); + Assert.Equal("read", Assert.IsType(query).Text); + } + + [Fact] + public void EndToEnd_DotStarBetweenLiterals() + { + // Goo.*Bar -> All(Literal("goo"), Literal("bar")) + var query = RegexQueryCompiler.Compile("Goo.*Bar")!; + var all = Assert.IsType(query); + Assert.Equal(2, all.Children.Length); + Assert.Equal("goo", Assert.IsType(all.Children[0]).Text); + Assert.Equal("bar", Assert.IsType(all.Children[1]).Text); + } + + [Fact] + public void EndToEnd_DotPlusBetweenLiterals() + { + // Goo.+Bar -> All(Literal("goo"), Literal("bar")) + var query = RegexQueryCompiler.Compile("Goo.+Bar")!; + var all = Assert.IsType(query); + Assert.Equal(2, all.Children.Length); + Assert.Equal("goo", Assert.IsType(all.Children[0]).Text); + Assert.Equal("bar", Assert.IsType(all.Children[1]).Text); + } + + [Fact] + public void EndToEnd_ComplexPattern() + { + // (Get|Set)(Value|Item)s? -> All(Any("get","set"), Any("value","item")) + var query = RegexQueryCompiler.Compile("(Get|Set)(Value|Item)s?")!; + Assert.True(query.HasLiterals); + + var all = Assert.IsType(query); + Assert.Equal(2, all.Children.Length); + + var first = Assert.IsType(all.Children[0]); + Assert.Equal("get", Assert.IsType(first.Children[0]).Text); + Assert.Equal("set", Assert.IsType(first.Children[1]).Text); + + var second = Assert.IsType(all.Children[1]); + Assert.Equal("value", Assert.IsType(second.Children[0]).Text); + Assert.Equal("item", Assert.IsType(second.Children[1]).Text); + } + + [Fact] + public void EndToEnd_AlternationOfPureDotStar_ReturnsNull() + => Assert.Null(RegexQueryCompiler.Compile("(.*|.+)")); + + [Fact] + public void EndToEnd_AnchorWithLiteral() + { + // ^Goo$ -> All(Literal("goo")) -> Literal("goo") + var query = RegexQueryCompiler.Compile("^Goo$")!; + Assert.Equal("goo", Assert.IsType(query).Text); + } + + [Fact] + public void EndToEnd_MixedLiteralsAndWildcards() + { + // Read.Line -> All(Literal("read"), Literal("line")) + var query = RegexQueryCompiler.Compile("Read.Line")!; + var all = Assert.IsType(query); + Assert.Equal(2, all.Children.Length); + Assert.Equal("read", Assert.IsType(all.Children[0]).Text); + Assert.Equal("line", Assert.IsType(all.Children[1]).Text); + } + + #endregion +} diff --git a/src/Features/Test/TypeHierarchy/TypeHierarchyServiceTests.cs b/src/Features/Test/TypeHierarchy/TypeHierarchyServiceTests.cs new file mode 100644 index 000000000000..53e1739fb3fb --- /dev/null +++ b/src/Features/Test/TypeHierarchy/TypeHierarchyServiceTests.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.TypeHierarchy; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.TypeHierarchy; + +[UseExportProvider] +public sealed class TypeHierarchyServiceTests +{ + [Theory] + [InlineData(LanguageNames.CSharp)] + [InlineData(LanguageNames.VisualBasic)] + public async Task GetBaseTypesAndInterfacesAsync_ReturnsExpectedBaseTypes(string language) + { + using var workspace = CreateWorkspace( + language, + csharpMarkup: """ + interface IRoot { } + class Base { } + class $$Derived : Base, IRoot { } + """, + vbMarkup: """ + Interface IRoot + End Interface + + Class Base + End Class + + Class $$Derived + Inherits Base + Implements IRoot + End Class + """); + + var (_, service, symbol) = await GetTypeSymbolWithServiceAsync(workspace); + var baseTypes = service.GetBaseTypesAndInterfaces(symbol); + var baseTypeNames = baseTypes.Select(static t => t.Name).ToImmutableArray(); + + Assert.Contains("Base", baseTypeNames); + Assert.Contains("IRoot", baseTypeNames); + } + + [Theory] + [InlineData(LanguageNames.CSharp)] + [InlineData(LanguageNames.VisualBasic)] + public async Task GetDerivedTypesAndImplementationsAsync_ReturnsExpectedDerivedTypes(string language) + { + using var workspace = CreateWorkspace( + language, + csharpMarkup: """ + class $$Base { } + class Mid : Base { } + class Derived : Mid { } + """, + vbMarkup: """ + Class $$Base + End Class + + Class Mid + Inherits Base + End Class + + Class Derived + Inherits Mid + End Class + """); + + var (document, service, symbol) = await GetTypeSymbolWithServiceAsync(workspace); + var derivedTypes = await service.GetDerivedTypesAndImplementationsAsync( + document.Project.Solution, + symbol, + transitive: true, + CancellationToken.None); + + AssertEx.SetEqual(["Mid", "Derived"], derivedTypes.Select(static t => t.Name)); + } + + [Theory] + [InlineData(LanguageNames.CSharp)] + [InlineData(LanguageNames.VisualBasic)] + public async Task GetBaseTypesAndInterfacesAsync_ForInterface_ReturnsExpectedBaseInterfaces(string language) + { + using var workspace = CreateWorkspace( + language, + csharpMarkup: """ + interface IRoot { } + interface $$IChild : IRoot { } + """, + vbMarkup: """ + Interface IRoot + End Interface + + Interface $$IChild + Inherits IRoot + End Interface + """); + + var (_, service, symbol) = await GetTypeSymbolWithServiceAsync(workspace); + var baseTypes = service.GetBaseTypesAndInterfaces(symbol); + var baseTypeNames = baseTypes.Select(static t => t.Name).ToImmutableArray(); + + Assert.Contains("IRoot", baseTypeNames); + } + + [Theory] + [InlineData(LanguageNames.CSharp)] + [InlineData(LanguageNames.VisualBasic)] + public async Task GetDerivedTypesAndImplementationsAsync_ForInterface_ReturnsDerivedInterfacesAndImplementations(string language) + { + using var workspace = CreateWorkspace( + language, + csharpMarkup: """ + interface $$IRoot { } + interface IChild : IRoot { } + class A : IRoot { } + class B : IChild { } + """, + vbMarkup: """ + Interface $$IRoot + End Interface + + Interface IChild + Inherits IRoot + End Interface + + Class A + Implements IRoot + End Class + + Class B + Implements IChild + End Class + """); + + var (document, service, symbol) = await GetTypeSymbolWithServiceAsync(workspace); + var derivedTypes = await service.GetDerivedTypesAndImplementationsAsync( + document.Project.Solution, + symbol, + transitive: true, + CancellationToken.None); + var derivedTypeNames = derivedTypes.Select(static t => t.Name).ToImmutableArray(); + + Assert.Contains("IChild", derivedTypeNames); + Assert.Contains("A", derivedTypeNames); + Assert.Contains("B", derivedTypeNames); + } + + private static TestWorkspace CreateWorkspace(string language, string csharpMarkup, string vbMarkup) + => language switch + { + LanguageNames.CSharp => TestWorkspace.CreateCSharp(csharpMarkup), + LanguageNames.VisualBasic => TestWorkspace.CreateVisualBasic(vbMarkup), + _ => throw new System.ArgumentOutOfRangeException(nameof(language), language, message: null), + }; + + private static async Task<(Document Document, ITypeHierarchyService Service, INamedTypeSymbol TypeSymbol)> GetTypeSymbolWithServiceAsync(TestWorkspace workspace) + { + var hostDocument = workspace.DocumentWithCursor; + var document = workspace.CurrentSolution.GetRequiredDocument(hostDocument.Id); + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, hostDocument.CursorPosition!.Value, cancellationToken: CancellationToken.None); + var typeSymbol = Assert.IsAssignableFrom(symbol); + + var service = document.GetRequiredLanguageService(); + return (document, service, typeSymbol); + } +} diff --git a/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs b/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs index 22d1a175d68d..e2c5cf7b9e2f 100644 --- a/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs +++ b/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs @@ -357,12 +357,6 @@ internal Guid EmitLibrary( targetFramework); } - internal static SourceText CreateText(string source, Encoding? encoding = null, SourceHashAlgorithm checksumAlgorithm = SourceHashAlgorithms.Default) - { - encoding ??= Encoding.UTF8; - return SourceText.From(new MemoryStream(encoding.GetBytesWithPreamble(source)), encoding, checksumAlgorithm); - } - internal Guid EmitLibrary( ProjectId projectId, IEnumerable<(SourceText text, string filePath)> sources, @@ -484,13 +478,16 @@ internal Guid EmitLibrary( return moduleId; } - internal static SourceText CreateText(string source) - => SourceText.From(source, Encoding.UTF8, SourceHashAlgorithms.Default); + internal static SourceText CreateText(string source, Encoding? encoding = null, SourceHashAlgorithm checksumAlgorithm = SourceHashAlgorithms.Default) + { + encoding ??= Encoding.UTF8; + return SourceText.From(new MemoryStream(encoding.GetBytesWithPreamble(source)), encoding, checksumAlgorithm); + } - internal static SourceText CreateTextFromFile(string path) + internal static SourceText CreateTextFromFile(string path, Encoding? encoding = null) { using var stream = File.OpenRead(path); - return SourceText.From(stream, Encoding.UTF8, SourceHashAlgorithms.Default); + return SourceText.From(stream, encoding ?? Encoding.UTF8, SourceHashAlgorithms.Default); } internal static TextSpan GetSpan(string str, string substr) diff --git a/src/Features/TestUtilities/EditAndContinue/MockHostWorkspaceProvider.cs b/src/Features/TestUtilities/EditAndContinue/MockHostWorkspaceProvider.cs index 66885cf7b105..4ff431bdf63d 100644 --- a/src/Features/TestUtilities/EditAndContinue/MockHostWorkspaceProvider.cs +++ b/src/Features/TestUtilities/EditAndContinue/MockHostWorkspaceProvider.cs @@ -10,13 +10,9 @@ namespace Microsoft.CodeAnalysis.EditAndContinue.UnitTests; [Export(typeof(IHostWorkspaceProvider)), PartNotDiscoverable, Shared] -internal sealed class MockHostWorkspaceProvider : IHostWorkspaceProvider +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class MockHostWorkspaceProvider() : IHostWorkspaceProvider { public Workspace Workspace { get; set; } = null!; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public MockHostWorkspaceProvider() - { - } } diff --git a/src/Features/TestUtilities/EditAndContinue/MockPdbMatchingSourceTextProvider.cs b/src/Features/TestUtilities/EditAndContinue/MockPdbMatchingSourceTextProvider.cs index c40e896847a2..fb60af4a4225 100644 --- a/src/Features/TestUtilities/EditAndContinue/MockPdbMatchingSourceTextProvider.cs +++ b/src/Features/TestUtilities/EditAndContinue/MockPdbMatchingSourceTextProvider.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.EditAndContinue.UnitTests; diff --git a/src/Features/TestUtilities/Microsoft.CodeAnalysis.Features.Test.Utilities.csproj b/src/Features/TestUtilities/Microsoft.CodeAnalysis.Features.Test.Utilities.csproj index 87019edd23f5..a731d0be4680 100644 --- a/src/Features/TestUtilities/Microsoft.CodeAnalysis.Features.Test.Utilities.csproj +++ b/src/Features/TestUtilities/Microsoft.CodeAnalysis.Features.Test.Utilities.csproj @@ -4,7 +4,7 @@ Library Microsoft.CodeAnalysis.Test.Utilities - $(NetVS);net472 + $(NetRoslynAll);net472 true false true @@ -35,7 +35,6 @@ - diff --git a/src/Features/VisualBasic/Portable/CallHierarchy/VisualBasicCallHierarchyService.vb b/src/Features/VisualBasic/Portable/CallHierarchy/VisualBasicCallHierarchyService.vb new file mode 100644 index 000000000000..96a8b66493be --- /dev/null +++ b/src/Features/VisualBasic/Portable/CallHierarchy/VisualBasicCallHierarchyService.vb @@ -0,0 +1,33 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports System.Composition +Imports System.Threading +Imports Microsoft.CodeAnalysis.CallHierarchy +Imports Microsoft.CodeAnalysis.Host.Mef + +Namespace Microsoft.CodeAnalysis.VisualBasic.CallHierarchy + + Friend Class VisualBasicCallHierarchyService + Inherits AbstractCallHierarchyService + + + + Public Sub New() + End Sub + + Protected Overrides Async Function GetOperationRootSyntaxAsync(syntaxReference As SyntaxReference, cancellationToken As CancellationToken) As Task(Of SyntaxNode) + Dim syntax = Await MyBase.GetOperationRootSyntaxAsync(syntaxReference, cancellationToken).ConfigureAwait(False) + + ' In VB, declaration syntax references point at statement nodes (for example, + ' MethodStatement/PropertyStatement) instead of the enclosing block node. GetOperation + ' returns null for the statement but non-null for the block, so we step to the parent. + If syntax.Parent IsNot Nothing Then + syntax = syntax.Parent + End If + + Return syntax + End Function + End Class +End Namespace diff --git a/src/Features/VisualBasic/Portable/TypeHierarchy/VisualBasicTypeHierarchyService.vb b/src/Features/VisualBasic/Portable/TypeHierarchy/VisualBasicTypeHierarchyService.vb new file mode 100644 index 000000000000..26e24309a8e8 --- /dev/null +++ b/src/Features/VisualBasic/Portable/TypeHierarchy/VisualBasicTypeHierarchyService.vb @@ -0,0 +1,19 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports System.Composition +Imports Microsoft.CodeAnalysis.Host.Mef +Imports Microsoft.CodeAnalysis.TypeHierarchy + +Namespace Microsoft.CodeAnalysis.VisualBasic.TypeHierarchy + + Friend Class VisualBasicTypeHierarchyService + Inherits AbstractTypeHierarchyService + + + + Public Sub New() + End Sub + End Class +End Namespace diff --git a/src/Features/VisualBasicTest/Microsoft.CodeAnalysis.VisualBasic.Features.UnitTests.vbproj b/src/Features/VisualBasicTest/Microsoft.CodeAnalysis.VisualBasic.Features.UnitTests.vbproj index 0b3377fb7935..c602eb7f19b5 100644 --- a/src/Features/VisualBasicTest/Microsoft.CodeAnalysis.VisualBasic.Features.UnitTests.vbproj +++ b/src/Features/VisualBasicTest/Microsoft.CodeAnalysis.VisualBasic.Features.UnitTests.vbproj @@ -4,7 +4,7 @@ Library - $(NetVSShared);net472 + $(NetRoslynWindowsTests);net472 diff --git a/src/Interactive/Host/Interactive/Core/InteractiveHost.LazyRemoteService.cs b/src/Interactive/Host/Interactive/Core/InteractiveHost.LazyRemoteService.cs index 17ba392a529d..183cfd25e953 100644 --- a/src/Interactive/Host/Interactive/Core/InteractiveHost.LazyRemoteService.cs +++ b/src/Interactive/Host/Interactive/Core/InteractiveHost.LazyRemoteService.cs @@ -180,10 +180,10 @@ private async Task TryStartAndInitializeProcessAsync(C newProcessId = 0; } - var clientStream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + var clientStream = NamedPipeUtil.CreateClient(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); JsonRpc? jsonRpc = null; - void ProcessExitedBeforeEstablishingConnection(object sender, EventArgs e) + void ProcessExitedBeforeEstablishingConnection(object? sender, EventArgs e) { Host.InteractiveHostProcessCreationFailed?.Invoke(null, TryGetExitCode(newProcess)); _cancellationSource.Cancel(); @@ -207,7 +207,7 @@ void ProcessExitedBeforeEstablishingConnection(object sender, EventArgs e) platformInfo = (await jsonRpc.InvokeWithCancellationAsync( nameof(Service.InitializeAsync), - new object[] { Host._replServiceProviderType.AssemblyQualifiedName }, + new object?[] { Host._replServiceProviderType.AssemblyQualifiedName }, cancellationToken).ConfigureAwait(false)).Deserialize(); } catch (Exception e) diff --git a/src/Interactive/Host/Interactive/Core/InteractiveHost.RemoteService.cs b/src/Interactive/Host/Interactive/Core/InteractiveHost.RemoteService.cs index d7bf21eac0c8..a2ee1e18a318 100644 --- a/src/Interactive/Host/Interactive/Core/InteractiveHost.RemoteService.cs +++ b/src/Interactive/Host/Interactive/Core/InteractiveHost.RemoteService.cs @@ -73,7 +73,7 @@ internal void HookAutoRestartEvent() } } - private void ProcessExitedHandler(object sender, EventArgs e) + private void ProcessExitedHandler(object? sender, EventArgs e) { _ = ProcessExitedHandlerAsync(); } diff --git a/src/Interactive/Host/Interactive/Core/InteractiveHost.Service.cs b/src/Interactive/Host/Interactive/Core/InteractiveHost.Service.cs index 1d852e5ec738..6671c9982f82 100644 --- a/src/Interactive/Host/Interactive/Core/InteractiveHost.Service.cs +++ b/src/Interactive/Host/Interactive/Core/InteractiveHost.Service.cs @@ -148,7 +148,7 @@ private Service(Func, object> invokeOnMainThread) AppDomain.CurrentDomain.ProcessExit += HandleProcessExit; } - private void HandleProcessExit(object sender, EventArgs e) + private void HandleProcessExit(object? sender, EventArgs e) { Dispose(); AppDomain.CurrentDomain.ProcessExit -= HandleProcessExit; @@ -170,7 +170,7 @@ public void Dispose() var assemblyLoader = new InteractiveAssemblyLoader(metadataFileProvider); var replServiceProviderType = Type.GetType(replServiceProviderTypeName); - var replServiceProvider = (ReplServiceProvider)Activator.CreateInstance(replServiceProviderType); + var replServiceProvider = (ReplServiceProvider)Activator.CreateInstance(replServiceProviderType!)!; var globals = new InteractiveScriptGlobals(Console.Out, replServiceProvider.ObjectFormatter); _serviceState = new ServiceState(assemblyLoader, metadataFileProvider, replServiceProvider, globals); @@ -223,7 +223,7 @@ public static async Task RunServerAsync(string pipeName, int clientProcessId, Fu return; } - var serverStream = new NamedPipeServerStream(pipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + var serverStream = NamedPipeUtil.CreateServer(pipeName, PipeDirection.InOut); await serverStream.WaitForConnectionAsync().ConfigureAwait(false); var jsonRpc = CreateRpc(serverStream, incomingCallTarget: new Service(invokeOnMainThread)); @@ -243,7 +243,7 @@ public static async Task RunServerAsync(string pipeName, int clientProcessId, Fu public async Task SetPathsAsync( string[] referenceSearchPaths, string[] sourceSearchPaths, - string? baseDirectory) + string baseDirectory) { var completionSource = new TaskCompletionSource(); lock (_lastTaskGuard) @@ -259,7 +259,7 @@ private async Task SetPathsAsync( TaskCompletionSource completionSource, string[]? referenceSearchPaths, string[]? sourceSearchPaths, - string? baseDirectory) + string baseDirectory) { var serviceState = GetServiceState(); var state = await ReportUnhandledExceptionIfAnyAsync(lastTask).ConfigureAwait(false); @@ -526,7 +526,7 @@ private async Task InitializeContextAsync( // Otherwise, platform assemblies are looked up in PlatformAssemblyPaths directly. var sdkDirectory = s_currentPlatformInfo.PlatformAssemblyPaths.IsEmpty ? RuntimeEnvironment.GetRuntimeDirectory() : null; - var rspDirectory = Path.GetDirectoryName(initializationFilePath); + var rspDirectory = Path.GetDirectoryName(initializationFilePath)!; var args = parser.Parse(new[] { "@" + initializationFilePath }, baseDirectory: rspDirectory, sdkDirectory, additionalReferenceDirectories: null); foreach (var error in args.Errors) @@ -730,7 +730,7 @@ private async Task ExecuteFileAsync( private static void DisplaySearchPaths(TextWriter writer, List attemptedFilePaths) { - var directories = attemptedFilePaths.Select(path => Path.GetDirectoryName(path)).ToArray(); + var directories = attemptedFilePaths.Select(path => Path.GetDirectoryName(path)!).ToArray(); var uniqueDirectories = new HashSet(directories); writer.WriteLine(uniqueDirectories.Count == 1 diff --git a/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj b/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj index 9098badd0229..a3775eead38f 100644 --- a/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj +++ b/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj @@ -1,14 +1,13 @@  - + Library Microsoft.CodeAnalysis.Interactive - netstandard2.0 - true + net472;$(NetVS) true true - + $(DefineConstants);MICROSOFT_CODEANALYSIS_THREADING_NO_CHANNELS @@ -43,6 +42,8 @@ + + @@ -51,6 +52,7 @@ + diff --git a/src/Interactive/HostTest/InteractiveHost.UnitTests.csproj b/src/Interactive/HostTest/InteractiveHost.UnitTests.csproj index e3306cfbb04d..15e995a8be3c 100644 --- a/src/Interactive/HostTest/InteractiveHost.UnitTests.csproj +++ b/src/Interactive/HostTest/InteractiveHost.UnitTests.csproj @@ -4,7 +4,7 @@ Library Roslyn.InteractiveHost.UnitTests - $(NetRoslyn) + net472 diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/AutoLoadProjectsInitializerTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/AutoLoadProjectsInitializerTests.cs new file mode 100644 index 000000000000..eb1dcc12d93f --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/AutoLoadProjectsInitializerTests.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.Extensions.Logging.Abstractions; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public sealed class AutoLoadProjectsInitializerTests : IDisposable +{ + private readonly TempRoot _tempRoot = new(); + + public void Dispose() + => _tempRoot.Dispose(); + + [Fact] + public void TryGetVSCodeSolutionSettings_SingleFolderDisableSuppressesAutoLoad() + { + var workspaceFolder = CreateWorkspaceFolder("single", """ + { + "dotnet.defaultSolution": "disable" + } + """); + + var (isLoadingDisabled, solutionPath) = AutoLoadProjectsInitializer.TryGetVSCodeSolutionSettings([workspaceFolder], NullLogger.Instance); + + Assert.True(isLoadingDisabled); + Assert.Null(solutionPath); + } + + [Fact] + public void TryGetVSCodeSolutionSettings_MultiFolderUsesFirstConfiguredSolution() + { + var firstFolder = CreateWorkspaceFolder("first", """ + { + "dotnet.defaultSolution": "disable" + } + """); + var secondFolder = CreateWorkspaceFolder("second", """ + { + "dotnet.defaultSolution": "eng/App.slnx" + } + """); + CreateSolutionFile(secondFolder, "eng/App.slnx"); + + var (isLoadingDisabled, solutionPath) = AutoLoadProjectsInitializer.TryGetVSCodeSolutionSettings([firstFolder, secondFolder], NullLogger.Instance); + + Assert.False(isLoadingDisabled); + Assert.Equal(Path.Combine(ProtocolConversions.GetDocumentFilePathFromUri(secondFolder.DocumentUri.GetRequiredParsedUri()), "eng", "App.slnx"), solutionPath); + } + + private string WriteSettingsFile(string settingsJson) + { + var folder = _tempRoot.CreateDirectory(); + var settingsDirectory = folder.CreateDirectory(".vscode"); + var settingsPath = Path.Combine(settingsDirectory.Path, "settings.json"); + File.WriteAllText(settingsPath, settingsJson); + return settingsPath; + } + + private WorkspaceFolder CreateWorkspaceFolder(string name, string settingsJson) + { + var settingsPath = WriteSettingsFile(settingsJson); + var folder = Path.GetDirectoryName(Path.GetDirectoryName(settingsPath))!; + + return new WorkspaceFolder + { + Name = name, + DocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(folder), + }; + } + + private static void CreateSolutionFile(WorkspaceFolder folder, string relativePath) + { + var folderPath = ProtocolConversions.GetDocumentFilePathFromUri(folder.DocumentUri.GetRequiredParsedUri()); + var solutionPath = Path.Combine(folderPath, relativePath); + Directory.CreateDirectory(Path.GetDirectoryName(solutionPath)!); + File.WriteAllText(solutionPath, string.Empty); + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/DefaultFileChangeWatcherTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/DefaultFileChangeWatcherTests.cs new file mode 100644 index 000000000000..1669fafaccf2 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/DefaultFileChangeWatcherTests.cs @@ -0,0 +1,682 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; +using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.CodeAnalysis.Test.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public sealed class SimpleFileChangeWatcherTests : IDisposable +{ + private readonly TimeSpan _fileChangeTimeout = TimeSpan.FromSeconds(1); + private readonly TempRoot _tempRoot = new(); + + public void Dispose() => _tempRoot.Dispose(); + + [Fact] + public void CreateContext_WithEmptyDirectories_DoesNotAddRootWatcher() + { + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([]); + + Assert.Empty(DefaultFileChangeWatcher.FileChangeContext.TestAccessor.GetRootFileWatchers((DefaultFileChangeWatcher.FileChangeContext)context)); + } + + [Fact] + public void CreateContext_WithExistingDirectory_AddsRootWatcher() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + + Assert.Single(DefaultFileChangeWatcher.FileChangeContext.TestAccessor.GetRootFileWatchers((DefaultFileChangeWatcher.FileChangeContext)context)); + } + + [Fact] + public void CreateContext_WithNonExistentDirectory_DoesNotAddRootWatcher() + { + var nonExistentPath = Path.Combine(TempRoot.Root, "NonExistent", "Directory", Guid.NewGuid().ToString()); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(nonExistentPath, extensionFilters: [])]); + + Assert.Empty(DefaultFileChangeWatcher.FileChangeContext.TestAccessor.GetRootFileWatchers((DefaultFileChangeWatcher.FileChangeContext)context)); + } + + [Fact] + public void CreateContext_WithMultipleDirectories_OnSameRoot_CreatesOneRootWatcher() + { + var rootDir = _tempRoot.CreateDirectory(); + var subDir1 = rootDir.CreateDirectory("sub1"); + var subDir2 = rootDir.CreateDirectory("sub2"); + var watcher = new DefaultFileChangeWatcher(); + + // Both directories are under the same root + using var context = watcher.CreateContext([ + new WatchedDirectory(subDir1.Path, extensionFilters: []), + new WatchedDirectory(subDir2.Path, extensionFilters: []) + ]); + + Assert.Single(DefaultFileChangeWatcher.FileChangeContext.TestAccessor.GetRootFileWatchers((DefaultFileChangeWatcher.FileChangeContext)context)); + } + + [Fact] + public void EnqueueWatchingFile_InWatchedDirectory_ReturnsNoOpWatcher() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "test.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + var watchedFile = context.EnqueueWatchingFile(filePath); + + // When a file is already covered by a directory watch, it returns NoOpWatchedFile + Assert.Same(NoOpWatchedFile.Instance, watchedFile); + } + + [Fact] + public void EnqueueWatchingFile_OutsideWatchedDirectory_ReturnsIndividualWatcher() + { + var watchedDir = _tempRoot.CreateDirectory(); + var otherDir = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(otherDir.Path, "test.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(watchedDir.Path, extensionFilters: [])]); + using var watchedFile = context.EnqueueWatchingFile(filePath); + + // When a file is not covered, it returns an actual watcher + Assert.NotSame(NoOpWatchedFile.Instance, watchedFile); + } + + [Fact] + public void EnqueueWatchingFile_WithExtensionFilter_MatchingExtension_ReturnsNoOpWatcher() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "test.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + using var watchedFile = context.EnqueueWatchingFile(filePath); + + Assert.Same(NoOpWatchedFile.Instance, watchedFile); + } + + [Fact] + public void EnqueueWatchingFile_WithExtensionFilter_NonMatchingExtension_ReturnsIndividualWatcher() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "test.txt"); + var watcher = new DefaultFileChangeWatcher(); + + // Only watching for .cs files + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + using var watchedFile = context.EnqueueWatchingFile(filePath); + + // .txt file is not covered by .cs filter, so it gets an individual watcher + Assert.NotSame(NoOpWatchedFile.Instance, watchedFile); + } + + [Fact] + public void EnqueueWatchingFile_MultipleTimesForSameFile_AllReturnDisposableWatchers() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "multi.txt"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([]); + + using var watcher1 = context.EnqueueWatchingFile(filePath); + using var watcher2 = context.EnqueueWatchingFile(filePath); + + // Both should be valid individual watchers + Assert.NotSame(NoOpWatchedFile.Instance, watcher1); + Assert.NotSame(NoOpWatchedFile.Instance, watcher2); + Assert.NotSame(watcher1, watcher2); + } + + [Fact] + public void EnqueueWatchingFile_InNestedDirectory_ReturnsNoOpWatcher() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var subDirectory = tempDirectory.CreateDirectory("subdir"); + var filePath = Path.Combine(subDirectory.Path, "nested.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + using var watchedFile = context.EnqueueWatchingFile(filePath); + + // File in subdirectory should be covered by parent directory watch + Assert.Same(NoOpWatchedFile.Instance, watchedFile); + } + + [Fact] + public void EnqueueWatchingFile_WithNonExistentDirectory_HandlesGracefully() + { + var watcher = new DefaultFileChangeWatcher(); + var nonExistentPath = Path.Combine(TempRoot.Root, "NonExistent", "file.cs"); + + using var context = watcher.CreateContext([]); + + // Should not throw, though the file won't actually be watched until the directory exists + using var watchedFile = context.EnqueueWatchingFile(nonExistentPath); + Assert.NotNull(watchedFile); + } + + [Fact] + public void EnqueueWatchingFile_WithMultipleExtensionFilters_MatchesAny() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var csFilePath = Path.Combine(tempDirectory.Path, "test.cs"); + var vbFilePath = Path.Combine(tempDirectory.Path, "test.vb"); + var txtFilePath = Path.Combine(tempDirectory.Path, "test.txt"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs", ".vb"])]); + + using var csWatcher = context.EnqueueWatchingFile(csFilePath); + using var vbWatcher = context.EnqueueWatchingFile(vbFilePath); + using var txtWatcher = context.EnqueueWatchingFile(txtFilePath); + + // .cs and .vb should be covered by directory watch + Assert.Same(NoOpWatchedFile.Instance, csWatcher); + Assert.Same(NoOpWatchedFile.Instance, vbWatcher); + + // .txt should need individual watch + Assert.NotSame(NoOpWatchedFile.Instance, txtWatcher); + } + + [Fact] + public void EnqueueWatchingFile_DeeplyNestedFile_ReturnsNoOpWatcher() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var level1 = tempDirectory.CreateDirectory("level1"); + var level2 = level1.CreateDirectory("level2"); + var level3 = level2.CreateDirectory("level3"); + var filePath = Path.Combine(level3.Path, "deep.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + using var watchedFile = context.EnqueueWatchingFile(filePath); + + // Deeply nested file should still be covered by root directory watch + Assert.Same(NoOpWatchedFile.Instance, watchedFile); + } + + [Fact] + public void EnqueueWatchingFile_SiblingDirectory_NotCovered() + { + var rootDir = _tempRoot.CreateDirectory(); + var watchedDir = rootDir.CreateDirectory("watched"); + var siblingDir = rootDir.CreateDirectory("sibling"); + var filePath = Path.Combine(siblingDir.Path, "test.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(watchedDir.Path, extensionFilters: [])]); + using var watchedFile = context.EnqueueWatchingFile(filePath); + + // File in sibling directory should not be covered + Assert.NotSame(NoOpWatchedFile.Instance, watchedFile); + } + + [Fact] + public void EnqueueWatchingFile_ParentDirectory_NotCovered() + { + var rootDir = _tempRoot.CreateDirectory(); + var watchedDir = rootDir.CreateDirectory("subdir"); + var filePath = Path.Combine(rootDir.Path, "test.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(watchedDir.Path, extensionFilters: [])]); + using var watchedFile = context.EnqueueWatchingFile(filePath); + + // File in parent directory should not be covered + Assert.NotSame(NoOpWatchedFile.Instance, watchedFile); + } + + [Fact] + public void EnqueueWatchingFile_DisposeThenEnqueueAgain_Works() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "test.txt"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([]); + + using var watcher1 = context.EnqueueWatchingFile(filePath); + watcher1.Dispose(); + + using var watcher2 = context.EnqueueWatchingFile(filePath); + Assert.NotSame(NoOpWatchedFile.Instance, watcher2); + } + + #region File System Event Tests + + /// + /// Helper method to wait for a file change event with timeout. + /// + private static async Task WaitForFileChangeAsync(Task fileChangeTask, TimeSpan timeout) + { + var completed = await Task.WhenAny(fileChangeTask, Task.Delay(timeout)); + return completed == fileChangeTask; + } + + private static async Task WaitForAllFileChangesAsync(Task[] fileChangeTasks, TimeSpan timeout) + { + var delay = Task.Delay(timeout); + var completed = await Task.WhenAny(Task.WhenAll(fileChangeTasks), delay); + return completed != delay; + } + + private static Task ListenForFileChangeAsync(DefaultFileChangeWatcher.FileChangeContext context, string filePath) + { + var eventSource = new TaskCompletionSource(); + + context.FileChanged += (sender, path) => + { + if (path == filePath) + eventSource.TrySetResult(); + }; + + return eventSource.Task; + } + + [Fact] + public async Task FileCreated_InWatchedDirectory_RaisesFileChangedEvent() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "created.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, filePath); + + // Create the file + File.WriteAllText(filePath, "initial content"); + + // Wait for the event + var eventFired = await WaitForFileChangeAsync(fileChangeTask, _fileChangeTimeout); + + Assert.True(eventFired, "FileChanged event should fire when a file is created"); + } + + [Fact] + public async Task FileModified_InWatchedDirectory_RaisesFileChangedEvent() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "modified.cs"); + var watcher = new DefaultFileChangeWatcher(); + + // Create file first before setting up the watcher + File.WriteAllText(filePath, "initial content"); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, filePath); + + // Modify the file + File.WriteAllText(filePath, "modified content"); + + // Wait for the event + var eventFired = await WaitForFileChangeAsync(fileChangeTask, _fileChangeTimeout); + + Assert.True(eventFired, "FileChanged event should fire when a file is modified"); + } + + [Fact] + public async Task FileDeleted_InWatchedDirectory_RaisesFileChangedEvent() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "deleted.cs"); + var watcher = new DefaultFileChangeWatcher(); + + // Create file first before setting up the watcher + File.WriteAllText(filePath, "content to delete"); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, filePath); + + // Delete the file + File.Delete(filePath); + + // Wait for the event + var eventFired = await WaitForFileChangeAsync(fileChangeTask, _fileChangeTimeout); + + Assert.True(eventFired, "FileChanged event should fire when a file is deleted"); + } + + [Fact] + public async Task FileCreated_WithMatchingExtensionFilter_RaisesFileChangedEvent() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "filtered.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, filePath); + + // Create a .cs file (should match filter) + File.WriteAllText(filePath, "content"); + + // Wait for the event + var eventFired = await WaitForFileChangeAsync(fileChangeTask, _fileChangeTimeout); + + Assert.True(eventFired, "FileChanged event should fire for files matching extension filter"); + } + + [Fact] + public async Task FileCreated_WithNonMatchingExtensionFilter_DoesNotRaiseFileChangedEvent() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var txtFilePath = Path.Combine(tempDirectory.Path, "filtered.txt"); + var watcher = new DefaultFileChangeWatcher(); + + // Only watching for .cs files + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, txtFilePath); + + // Create a .txt file (should not match filter) + File.WriteAllText(txtFilePath, "content"); + + // Wait a bit to ensure no event fires + await Task.Delay(_fileChangeTimeout); + + Assert.False(fileChangeTask.IsCompleted, "FileChanged event should NOT fire for files not matching extension filter"); + } + + [Fact] + public async Task FileCreated_InSubdirectory_RaisesFileChangedEvent() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var subDirectory = tempDirectory.CreateDirectory("subdir"); + var filePath = Path.Combine(subDirectory.Path, "nested.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, filePath); + + // Create file in subdirectory + File.WriteAllText(filePath, "nested content"); + + // Wait for the event + var eventFired = await WaitForFileChangeAsync(fileChangeTask, _fileChangeTimeout); + + Assert.True(eventFired, "FileChanged event should fire for files created in subdirectories"); + } + + [Fact] + public async Task IndividualFileWatch_FileCreated_RaisesFileChangedEvent() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "individual.txt"); + var watcher = new DefaultFileChangeWatcher(); + + // Create context without directory watches + using var context = watcher.CreateContext([]); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, filePath); + + // Watch the specific file + using var watchedFile = context.EnqueueWatchingFile(filePath); + + // Create the file + File.WriteAllText(filePath, "individual file content"); + + // Wait for the event + var eventFired = await WaitForFileChangeAsync(fileChangeTask, _fileChangeTimeout); + + Assert.True(eventFired, "FileChanged event should fire for individually watched files"); + } + + [Fact] + public async Task IndividualFileWatch_FileModified_RaisesFileChangedEvent() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "individual_modify.txt"); + var watcher = new DefaultFileChangeWatcher(); + + // Create the file first + File.WriteAllText(filePath, "initial content"); + + // Create context without directory watches + using var context = watcher.CreateContext([]); + + // Watch the specific file + using var watchedFile = context.EnqueueWatchingFile(filePath); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, filePath); + + // Modify the file + File.WriteAllText(filePath, "modified content"); + + // Wait for the event + var eventFired = await WaitForFileChangeAsync(fileChangeTask, _fileChangeTimeout); + + Assert.True(eventFired, "FileChanged event should fire when individually watched file is modified"); + } + + [Fact] + public async Task IndividualFileWatch_AfterDispose_DoesNotRaiseEvent() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "disposed.txt"); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([]); + + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, filePath); + + // Watch and then immediately dispose + var watchedFile = context.EnqueueWatchingFile(filePath); + watchedFile.Dispose(); + + // Small delay to ensure dispose completes + await Task.Delay(TimeSpan.FromMilliseconds(200)); + + // Create the file after disposing the watch + File.WriteAllText(filePath, "content after dispose"); + + // Wait to see if any events fire + await Task.Delay(_fileChangeTimeout); + + Assert.False(fileChangeTask.IsCompleted, "FileChanged event should NOT fire after individual file watch is disposed"); + } + + [Fact] + public async Task MultipleFileChanges_AllRaiseEvents() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var watcher = new DefaultFileChangeWatcher(); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + + // Create file paths first + var file1 = Path.Combine(tempDirectory.Path, "file1.cs"); + var file2 = Path.Combine(tempDirectory.Path, "file2.cs"); + var file3 = Path.Combine(tempDirectory.Path, "file3.cs"); + + var fileChangeTasks = new[] + { + ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, file1), + ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, file2), + ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, file3) + }; + + File.WriteAllText(file1, "content1"); + await Task.Delay(100); // Small delay between operations + File.WriteAllText(file2, "content2"); + await Task.Delay(100); + File.WriteAllText(file3, "content3"); + + // Wait for all events + var allEventsFired = await WaitForAllFileChangesAsync(fileChangeTasks, _fileChangeTimeout); + + Assert.True(allEventsFired, "Should receive events for all file changes"); + } + + [Fact] + public async Task FileRenamed_InWatchedDirectory_FireEventForOriginalPath() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var originalPath = Path.Combine(tempDirectory.Path, "original.cs"); + var renamedPath = Path.Combine(tempDirectory.Path, "renamed.cs"); + var watcher = new DefaultFileChangeWatcher(); + + // Create original file + File.WriteAllText(originalPath, "content"); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, originalPath); + + // Rename the file + File.Move(originalPath, renamedPath); + + // Wait for the event + var eventFired = await WaitForFileChangeAsync(fileChangeTask, _fileChangeTimeout); + + Assert.True(eventFired, "FileChanged event should fire when a file is renamed"); + } + + [Fact] + public async Task FileRenamed_InWatchedDirectory_FireEventForRenamedPath() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var originalPath = Path.Combine(tempDirectory.Path, "original.cs"); + var renamedPath = Path.Combine(tempDirectory.Path, "renamed.cs"); + var watcher = new DefaultFileChangeWatcher(); + + // Create original file + File.WriteAllText(originalPath, "content"); + + using var context = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + var fileChangeTask = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context, renamedPath); + + // Rename the file + File.Move(originalPath, renamedPath); + + // Wait for the event + var eventFired = await WaitForFileChangeAsync(fileChangeTask, _fileChangeTimeout); + + Assert.True(eventFired, "FileChanged event should fire when a file is renamed"); + } + + #endregion + + #region Shared Watcher Tests + + [Fact] + public void SharedWatcher_MultipleContexts_ShareSameRootWatcher() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var watcher = new DefaultFileChangeWatcher(); + + using var context1 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + using var context2 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + + // Both contexts should have acquired 1 root path + Assert.Single(DefaultFileChangeWatcher.FileChangeContext.TestAccessor.GetRootFileWatchers((DefaultFileChangeWatcher.FileChangeContext)context1)); + Assert.Single(DefaultFileChangeWatcher.FileChangeContext.TestAccessor.GetRootFileWatchers((DefaultFileChangeWatcher.FileChangeContext)context2)); + + // The watcher should only have 1 shared root watcher + Assert.Single(DefaultFileChangeWatcher.TestAccessor.GetWatchedRootPaths(watcher)); + } + + [Fact] + public void SharedWatcher_DisposingOneContext_KeepsWatcherForOther() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var watcher = new DefaultFileChangeWatcher(); + + var context1 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + var context2 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + + // Shared watcher should exist + Assert.Single(DefaultFileChangeWatcher.TestAccessor.GetWatchedRootPaths(watcher)); + + // Dispose context1 + context1.Dispose(); + + // Shared watcher should still exist for context2 + Assert.Single(DefaultFileChangeWatcher.TestAccessor.GetWatchedRootPaths(watcher)); + + // Dispose context2 + context2.Dispose(); + + // Now shared watcher should be disposed + Assert.Empty(DefaultFileChangeWatcher.TestAccessor.GetWatchedRootPaths(watcher)); + } + + [Fact] + public async Task SharedWatcher_MultipleContexts_BothReceiveEvents() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "test.cs"); + var watcher = new DefaultFileChangeWatcher(); + + using var context1 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + using var context2 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + + var fileChangeTasks = new[] + { + ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context1, filePath), + ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context2, filePath) + }; + + // Create file + File.WriteAllText(filePath, "content"); + + // Both contexts should receive the event + var allEventsFired = await WaitForAllFileChangesAsync(fileChangeTasks, _fileChangeTimeout); + + Assert.True(allEventsFired, "Both contexts should have received FileChanged events"); + } + + [Fact] + public async Task SharedWatcher_DisposedContext_DoesNotReceiveEvents() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var filePath = Path.Combine(tempDirectory.Path, "test.cs"); + var watcher = new DefaultFileChangeWatcher(); + + var context1 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + using var context2 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [".cs"])]); + + var context1Events = new List(); + var context2Received = ListenForFileChangeAsync((DefaultFileChangeWatcher.FileChangeContext)context2, filePath); + + context1.FileChanged += (sender, path) => context1Events.Add(path); + + // Dispose context1 before creating file + context1.Dispose(); + + // Create file + File.WriteAllText(filePath, "content"); + + // Only context2 should receive the event + var context2ReceivedEvent = await WaitForFileChangeAsync(context2Received, _fileChangeTimeout); + + Assert.True(context2ReceivedEvent, "Context 2 should receive FileChanged event"); + Assert.DoesNotContain(filePath, context1Events); + } + + [Fact] + public void SharedWatcher_NewContextAfterDispose_CreatesNewWatcher() + { + var tempDirectory = _tempRoot.CreateDirectory(); + var watcher = new DefaultFileChangeWatcher(); + + // Create and dispose first context + var context1 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + context1.Dispose(); + + Assert.Empty(DefaultFileChangeWatcher.TestAccessor.GetWatchedRootPaths(watcher)); + + // Create new context - should create a new watcher + using var context2 = watcher.CreateContext([new WatchedDirectory(tempDirectory.Path, extensionFilters: [])]); + + Assert.Single(DefaultFileChangeWatcher.TestAccessor.GetWatchedRootPaths(watcher)); + } + + #endregion +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsEntryPointDiscoveryTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsEntryPointDiscoveryTests.cs new file mode 100644 index 000000000000..d411c6d3e1b5 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsEntryPointDiscoveryTests.cs @@ -0,0 +1,1011 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Miscellaneous; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.UnitTests; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Test.Utilities; +using Roslyn.Utilities; +using StreamJsonRpc; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public sealed class FileBasedProgramsEntryPointDiscoveryTests : AbstractLanguageServerProtocolTests, IDisposable +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly ILoggerFactory _loggerFactory; + private readonly TestOutputLoggerProvider _loggerProvider; + private readonly TempRoot _tempRoot; + private readonly TempDirectory _mefCacheDirectory; + + private readonly List _additionalDirectoriesToDelete = []; + + public FileBasedProgramsEntryPointDiscoveryTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + _testOutputHelper = testOutputHelper; + _loggerProvider = new TestOutputLoggerProvider(testOutputHelper); + _loggerFactory = new LoggerFactory([_loggerProvider]); + _tempRoot = new(); + _mefCacheDirectory = _tempRoot.CreateDirectory(); + } + + protected override async ValueTask CreateExportProviderAsync() + { + AsynchronousOperationListenerProvider.Enable(enable: true); + + var (exportProvider, _) = await LanguageServerTestComposition.CreateExportProviderAsync( + _loggerFactory, + includeDevKitComponents: false, + cacheDirectory: _mefCacheDirectory.Path, + extensionPaths: []); + + return exportProvider; + } + + public void Dispose() + { + _tempRoot.Dispose(); + _loggerProvider.Dispose(); + _loggerFactory.Dispose(); + + foreach (var directory in _additionalDirectoriesToDelete) + { + if (Directory.Exists(directory)) + Directory.Delete(directory, recursive: true); + } + } + + private void DeferDeleteCacheDirectory(string workspacePath) + { + _additionalDirectoriesToDelete.Add(VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(workspacePath)); + } + + /// Verify that multiple invocations of 'actualFactory' result in the same 'expected' sequence. + private void AssertSequenceEqualAndStable(IEnumerable expected, Func> actualFactory) + { + AssertEx.SequenceEqual(expected, actualFactory()); + AssertEx.SequenceEqual(expected, actualFactory()); + } + + [Fact] + public async Task TestDiscovery_Simple() + { + // Simple case + // tempDir/ + // App.cs + // Ordinary.cs + + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + var appText = """ + #!/usr/bin/env dotnet + #:sdk Microsoft.Net.SDK + Console.WriteLine("Hello World"); + """; + var appFile = tempDir.CreateFile("App.cs").WriteAllText(appText); + // Note: having '#:' is not enough for discovery to detect a file. The file needs to start with '#!'. + var ordinaryText = """ + #:sdk Microsoft.Net.Sdk + public class Ordinary { } + """; + var ordinaryFile = tempDir.CreateFile("Ordinary.cs").WriteAllText(ordinaryText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + + var discovery = testLspServer.GetRequiredLspService(); + AssertSequenceEqualAndStable([appFile.Path], () => discovery.FindEntryPoints(tempDir.Path)); + + // Changed but still has '#!' + appFile.WriteAllText(appText + """ + + Console.WriteLine("Additional content"); + """); + AssertEx.SequenceEqual([appFile.Path], discovery.FindEntryPoints(tempDir.Path)); + + // Deleted from disk + File.Delete(appFile.Path); + AssertEx.Empty(discovery.FindEntryPoints(tempDir.Path)); + + // Put back on disk + appFile.WriteAllText(appText); + AssertEx.SequenceEqual([appFile.Path], discovery.FindEntryPoints(tempDir.Path)); + + // Changed and no longer has '#!' + appFile.WriteAllText(""" + Console.WriteLine("No more #! at start of file"); + """); + AssertEx.Empty(discovery.FindEntryPoints(tempDir.Path)); + + // Changed and again has '#!' + appFile.WriteAllText(appText); + AssertEx.SequenceEqual([appFile.Path], discovery.FindEntryPoints(tempDir.Path)); + } + + [Fact] + public async Task TestDiscovery_IgnoredFolders() + { + // Demonstrate ignored folders behavior + // tempDir/ + // artifacts/App1.cs + // App2.cs + + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + var artifactsDir = tempDir.CreateDirectory("artifacts"); + var app1Text = """ + #!/usr/bin/env dotnet + #:sdk Microsoft.Net.SDK + Console.WriteLine("Hello World"); + """; + var app1File = artifactsDir.CreateFile("App1.cs").WriteAllText(app1Text); + + var app2Text = app1Text; + var app2File = tempDir.CreateFile("App2.cs").WriteAllText(app2Text); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + + var discovery = testLspServer.GetRequiredLspService(); + AssertSequenceEqualAndStable([app2File.Path], () => discovery.FindEntryPoints(tempDir.Path)); + } + + [Fact] + public async Task TestDiscovery_CsprojInCone() + { + // Demonstrate csproj-in-cone behavior + // tempDir/ + // Project/ + // Project.csproj + // Program.cs + // App.cs + + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + var projectDir = tempDir.CreateDirectory("Project"); + var csprojFile = projectDir.CreateFile("Project.csproj"); + + var appText = """ + #!/usr/bin/env dotnet + #:sdk Microsoft.Net.SDK + Console.WriteLine("Hello World"); + """; + var programFile = projectDir.CreateFile("Program.cs").WriteAllText(appText); + var appFile = tempDir.CreateFile("App1.cs").WriteAllText(appText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + + var discovery = testLspServer.GetRequiredLspService(); + AssertSequenceEqualAndStable([appFile.Path], () => discovery.FindEntryPoints(tempDir.Path)); + + // Delete the csproj file + File.Delete(csprojFile.Path); + AssertSequenceEqualAndStable([appFile.Path, programFile.Path], () => discovery.FindEntryPoints(tempDir.Path)); + } + + [Fact] + public async Task TestDiscovery_Option_EnableFileBasedPrograms_True() + { + // Ensure discovery occurs when relevant options are enabled + // Note: the option is checked in the higher level API, so we need to verify the effects in project system. + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + var appText = """ + #!/usr/bin/env dotnet + #:sdk Microsoft.Net.SDK + Console.WriteLine("Hello World"); + """; + var appFile = tempDir.CreateFile("App1.cs").WriteAllText(appText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + OptionUpdater = options => options.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, true), + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + + var discovery = testLspServer.GetRequiredLspService(); + await discovery.FindAndLoadEntryPointsAsync(); + await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(CreateAbsoluteDocumentUri(appFile.Path), testLspServer); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.NotNull(document); + } + + [Fact] + public async Task TestDiscovery_Option_EnableFileBasedPrograms_False() + { + // Ensure discovery doesn't occur when 'dotnet.projects.enableFileBasedPrograms: false' is set + // Note: the option is checked in the higher level API, so we need to verify the effects in project system. + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + var appText = """ + #!/usr/bin/env dotnet + #:sdk Microsoft.Net.SDK + Console.WriteLine("Hello World"); + """; + var appFile = tempDir.CreateFile("App1.cs").WriteAllText(appText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + OptionUpdater = options => options.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, false), + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + + var discovery = testLspServer.GetRequiredLspService(); + await discovery.FindAndLoadEntryPointsAsync(); + await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + var (workspace, document) = await GetLspWorkspaceAndDocumentAsync(CreateAbsoluteDocumentUri(appFile.Path), testLspServer); + Assert.Null(workspace); + Assert.Null(document); + } + + [Fact] + public async Task TestDiscovery_Option_EnableAutomaticDiscovery_False() + { + // Ensure discovery doesn't occur when 'dotnet.fileBasedApps.enableAutomaticDiscovery: false' is set + // Note: the option is checked in the higher level API, so we need to verify the effects in project system. + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + var appText = """ + #!/usr/bin/env dotnet + #:sdk Microsoft.Net.SDK + Console.WriteLine("Hello World"); + """; + var appFile = tempDir.CreateFile("App1.cs").WriteAllText(appText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + OptionUpdater = options => options.SetGlobalOption(FileBasedAppsOptionsStorage.EnableAutomaticDiscovery, false), + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + + var discovery = testLspServer.GetRequiredLspService(); + await discovery.FindAndLoadEntryPointsAsync(); + await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + var (workspace, document) = await GetLspWorkspaceAndDocumentAsync(CreateAbsoluteDocumentUri(appFile.Path), testLspServer); + Assert.Null(workspace); + Assert.Null(document); + } + + [Fact] + public async Task TestDiscovery_UTF8_BOM() + { + // File starting with UTF-8 BOM followed by '#!' should be discovered + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + var appText = """ + #!/usr/bin/env dotnet + #:sdk Microsoft.Net.SDK + Console.WriteLine("Hello World"); + + """; + var bomAppText = "\uFEFF" + appText; + var appFile = tempDir.CreateFile("App.cs").WriteAllText(bomAppText); + var ordinaryFile = tempDir.CreateFile("Ordinary.cs").WriteAllText("public class Ordinary { }"); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + + var discovery = testLspServer.GetRequiredLspService(); + AssertEx.SequenceEqual([appFile.Path], discovery.FindEntryPoints(tempDir.Path)); + } + + private static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(DocumentUri uri, TestLspServer testLspServer) + { + var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(uri), CancellationToken.None).ConfigureAwait(false); + return (workspace, document as Document); + } + + private static async Task<(Workspace workspace, Document document)> GetRequiredLspWorkspaceAndDocumentAsync(DocumentUri uri, TestLspServer testLspServer) + { + var (workspace, document) = await GetLspWorkspaceAndDocumentAsync(uri, testLspServer); + Assert.NotNull(workspace); + Assert.NotNull(document); + return (workspace, document); + } + + [Fact] + public async Task Swap_ReplaceFBAWithNonFBA() + { + // Swap an FBA out for non-FBA at the same path 'sub1/File1.cs'. + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + var discovery = testLspServer.GetRequiredLspService(); + + // Setup + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"sub1")); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/File1.cs"), FbaContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/File2.cs"), OrdinaryCsContent); + + // First discovery (no cache) + var firstResult = discovery.FindEntryPoints(tempDir.Path).ToArray(); + + // Edits + File.Move(Path.Combine(tempDir.Path, @"sub1/File1.cs"), Path.Combine(tempDir.Path, @"sub1/File4.cs")); + File.Move(Path.Combine(tempDir.Path, @"sub1/File2.cs"), Path.Combine(tempDir.Path, @"sub1/File1.cs")); + + // Discovery with cache + var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + // Delete cache + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path); + Directory.Delete(cacheDirectory, recursive: true); + + // Discovery without cache - should match + var uncachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + AssertEx.SequenceEqual(uncachedResult, cachedResult, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public async Task Swap_ReplaceNonFBAWithFBA() + { + // Swap a non-FBA out for FBA at the same path 'sub/File1.cs'. + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + var discovery = testLspServer.GetRequiredLspService(); + + // Setup + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"sub1")); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/File1.cs"), OrdinaryCsContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/File2.cs"), FbaContent); + + // First discovery (no cache) + var firstResult = discovery.FindEntryPoints(tempDir.Path).ToArray(); + + // Edits + File.Move(Path.Combine(tempDir.Path, @"sub1/File1.cs"), Path.Combine(tempDir.Path, @"sub1/File4.cs")); + File.Move(Path.Combine(tempDir.Path, @"sub1/File2.cs"), Path.Combine(tempDir.Path, @"sub1/File1.cs")); + + // Discovery with cache + var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + // Delete cache + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path); + Directory.Delete(cacheDirectory, recursive: true); + + // Discovery without cache — should match + var uncachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + AssertEx.SequenceEqual(uncachedResult, cachedResult, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public async Task Swap_ReplaceFBADirectoryWithNonFBADirectory() + { + // Swap a directory containing FBA out for a directory containing non-FBA at 'sub1/File1.cs'. + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + var discovery = testLspServer.GetRequiredLspService(); + + // Setup + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"sub1")); + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"sub2")); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/File1.cs"), FbaContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub2/File1.cs"), OrdinaryCsContent); + + // First discovery (no cache) + var firstResult = discovery.FindEntryPoints(tempDir.Path).ToArray(); + + // Edits + Directory.Move(Path.Combine(tempDir.Path, @"sub1"), Path.Combine(tempDir.Path, @"sub4")); + Directory.Move(Path.Combine(tempDir.Path, @"sub2"), Path.Combine(tempDir.Path, @"sub1")); + + // Discovery with cache - should match + var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + // Delete cache + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path); + Directory.Delete(cacheDirectory, recursive: true); + + // Discovery without cache + var uncachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + AssertEx.SequenceEqual(uncachedResult, cachedResult, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public async Task Swap_ReplaceNonFBADirectoryWithFBADirectory() + { + // Swap a directory containing non-FBA out for a directory containing FBA at the same path 'sub1/File1.cs'. + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + var discovery = testLspServer.GetRequiredLspService(); + + // Setup + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"sub1")); + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"sub2")); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/File1.cs"), OrdinaryCsContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub2/File1.cs"), FbaContent); + + // First discovery (no cache) + var firstResult = discovery.FindEntryPoints(tempDir.Path).ToArray(); + + // Edits + Directory.Move(Path.Combine(tempDir.Path, @"sub1"), Path.Combine(tempDir.Path, @"sub4")); + Directory.Move(Path.Combine(tempDir.Path, @"sub2"), Path.Combine(tempDir.Path, @"sub1")); + + // Discovery with cache + var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + // Delete cache + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path); + Directory.Delete(cacheDirectory, recursive: true); + + // Discovery without cache — should match + var uncachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + AssertEx.SequenceEqual(uncachedResult, cachedResult, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public async Task Fuzz_1() + { + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + var discovery = testLspServer.GetRequiredLspService(); + + // Setup + File.WriteAllText(Path.Combine(tempDir.Path, @"Fba0.cs"), FbaContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"Fba1.cs"), FbaContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"Ordinary2.cs"), OrdinaryCsContent); + + // First discovery (no cache) + var firstResult = discovery.FindEntryPoints(tempDir.Path).ToArray(); + + // Edits + File.WriteAllText(Path.Combine(tempDir.Path, @"New102.csproj"), CsprojContent); + File.Delete(Path.Combine(tempDir.Path, @"Fba0.cs")); + File.WriteAllText(Path.Combine(tempDir.Path, @"NewOrd22.cs"), OrdinaryCsContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"Ordinary2.cs"), OrdinaryCsContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"Ordinary2.cs"), FbaContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"NewOrd5.cs"), OrdinaryCsContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"New79.csproj"), CsprojContent); + + // Discovery with cache + var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + // Delete cache + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path); + Directory.Delete(cacheDirectory, recursive: true); + + // Discovery without cache — should match + var uncachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + AssertEx.SequenceEqual(cachedResult, uncachedResult, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public async Task Fuzz_2() + { + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + var discovery = testLspServer.GetRequiredLspService(); + + // Setup + File.WriteAllText(Path.Combine(tempDir.Path, @"Fba0.cs"), FbaContent); + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"deep/nested")); + File.WriteAllText(Path.Combine(tempDir.Path, @"deep/nested/Fba1.cs"), FbaContent); + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"deep/nested")); + File.WriteAllText(Path.Combine(tempDir.Path, @"deep/nested/Project2.csproj"), CsprojContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"Project3.csproj"), CsprojContent); + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"deep/nested/sub3")); + + // First discovery (no cache) + var firstResult = discovery.FindEntryPoints(tempDir.Path).ToArray(); + + // Edits + File.WriteAllText(Path.Combine(tempDir.Path, @"NewOrd40.cs"), OrdinaryCsContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"deep/nested/sub3/New52.csproj"), CsprojContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"deep/nested/NewOrd20.cs"), OrdinaryCsContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"deep/nested/Fba1.cs"), OrdinaryCsContent); + + // Discovery with cache + var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + // Delete cache + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path); + Directory.Delete(cacheDirectory, recursive: true); + + // Discovery without cache — should match + var uncachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + AssertEx.SequenceEqual(cachedResult, uncachedResult, StringComparer.OrdinalIgnoreCase); + } + + [Fact] + public async Task Fuzz_3() + { + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + var discovery = testLspServer.GetRequiredLspService(); + + // Setup + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"sub1")); + Directory.CreateDirectory(Path.Combine(tempDir.Path, @"sub1/sub3")); + File.WriteAllText(Path.Combine(tempDir.Path, @"Project0.csproj"), CsprojContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/sub3/Fba1.cs"), FbaContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/Fba2.cs"), FbaContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/Fba3.cs"), FbaContent); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/Ordinary4.cs"), OrdinaryCsContent); + + // First discovery (no cache) + var firstResult = discovery.FindEntryPoints(tempDir.Path).ToArray(); + + // Edits + File.Delete(Path.Combine(tempDir.Path, @"Project0.csproj")); + File.WriteAllText(Path.Combine(tempDir.Path, @"sub1/sub3/NewFba64.cs"), FbaContent); + + // Discovery with cache + var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + // Delete cache + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path); + Directory.Delete(cacheDirectory, recursive: true); + + // Discovery without cache — should match + var uncachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + AssertEx.SequenceEqual(cachedResult, uncachedResult, StringComparer.OrdinalIgnoreCase); + } + + #region Fuzzer + + private const string FbaContent = """ + #!/usr/bin/env dotnet + #:sdk Microsoft.Net.SDK + Console.WriteLine("hello"); + + """; + private const string OrdinaryCsContent = """ + public class C {} + + """; + private const string CsprojContent = ""; + + /// + /// Describes a single filesystem operation performed during a fuzz iteration. + /// + private abstract record FuzzOp + { + protected static string NormalizeForCSharp(string relativePath) => relativePath.Replace('\\', '/'); + + public abstract string ToCSharp(string tempDirVar); + + /// Creates a directory at the given relative path. + internal sealed record CreateDir(string RelativePath) : FuzzOp + { + public override string ToCSharp(string tempDirVar) => $"Directory.CreateDirectory(Path.Combine({tempDirVar}.Path, @\"{NormalizeForCSharp(RelativePath)}\"));"; + } + + /// Writes a .cs file with file-based-app content (starts with '#!'). + internal sealed record WriteFbaFile(string RelativePath) : FuzzOp + { + public override string ToCSharp(string tempDirVar) => $"File.WriteAllText(Path.Combine({tempDirVar}.Path, @\"{NormalizeForCSharp(RelativePath)}\"), FbaContent);"; + } + + /// Writes a .cs file without file-based-app content (no '#!' at start). + internal sealed record WriteOrdinaryCs(string RelativePath) : FuzzOp + { + public override string ToCSharp(string tempDirVar) => $"File.WriteAllText(Path.Combine({tempDirVar}.Path, @\"{NormalizeForCSharp(RelativePath)}\"), OrdinaryCsContent);"; + } + + /// Writes a .csproj file. + internal sealed record WriteCsproj(string RelativePath) : FuzzOp + { + public override string ToCSharp(string tempDirVar) => $"File.WriteAllText(Path.Combine({tempDirVar}.Path, @\"{NormalizeForCSharp(RelativePath)}\"), CsprojContent);"; + } + + /// Deletes a file. + internal sealed record DeleteFile(string RelativePath) : FuzzOp + { + public override string ToCSharp(string tempDirVar) => $"File.Delete(Path.Combine({tempDirVar}.Path, @\"{NormalizeForCSharp(RelativePath)}\"));"; + } + + /// Renames/moves a file. + internal sealed record RenameFile(string OldRelativePath, string NewRelativePath) : FuzzOp + { + public override string ToCSharp(string tempDirVar) => $"File.Move(Path.Combine({tempDirVar}.Path, @\"{NormalizeForCSharp(OldRelativePath)}\"), Path.Combine({tempDirVar}.Path, @\"{NormalizeForCSharp(NewRelativePath)}\"));"; + } + } + + /// + /// Tracks what files exist in the virtual workspace to enable the fuzzer + /// to generate valid operations (e.g. only delete files that exist). + /// + private sealed class FuzzWorkspace + { + private readonly string _rootPath; + private readonly HashSet _directories = new(StringComparer.OrdinalIgnoreCase); + private readonly HashSet _files = new(StringComparer.OrdinalIgnoreCase); + + public FuzzWorkspace(string rootPath) + { + _rootPath = rootPath; + _directories.Add(""); // root + } + + public IReadOnlyCollection Directories => _directories; + public IReadOnlyCollection Files => _files; + + public string FullPath(string relativePath) => Path.Combine(_rootPath, relativePath); + + public void Apply(FuzzOp op) + { + switch (op) + { + case FuzzOp.CreateDir createDir: + _directories.Add(createDir.RelativePath); + Directory.CreateDirectory(FullPath(createDir.RelativePath)); + break; + case FuzzOp.WriteFbaFile writeFba: + _files.Add(writeFba.RelativePath); + File.WriteAllText(FullPath(writeFba.RelativePath), FbaContent); + break; + case FuzzOp.WriteOrdinaryCs writeCs: + _files.Add(writeCs.RelativePath); + File.WriteAllText(FullPath(writeCs.RelativePath), OrdinaryCsContent); + break; + case FuzzOp.WriteCsproj writeCsproj: + _files.Add(writeCsproj.RelativePath); + File.WriteAllText(FullPath(writeCsproj.RelativePath), CsprojContent); + break; + case FuzzOp.DeleteFile deleteFile: + _files.Remove(deleteFile.RelativePath); + File.Delete(FullPath(deleteFile.RelativePath)); + break; + case FuzzOp.RenameFile rename: + _files.Remove(rename.OldRelativePath); + _files.Add(rename.NewRelativePath); + File.Move(FullPath(rename.OldRelativePath), FullPath(rename.NewRelativePath)); + break; + } + } + } + + private static readonly string[] s_dirNames = ["sub1", "sub2", "sub3", "deep" + Path.DirectorySeparatorChar + "nested"]; + + /// + /// Generates a random "setup" operation (creating directories and files). + /// + private static FuzzOp GenerateSetupOp(Random random, FuzzWorkspace workspace) + { + // Weighted: create dirs early, then files + var dirList = workspace.Directories.ToArray(); + if (dirList.Length < 4 && random.Next(3) == 0) + { + // Create a subdirectory + var parentDir = dirList[random.Next(dirList.Length)]; + var name = s_dirNames[random.Next(s_dirNames.Length)]; + var relativePath = parentDir.Length == 0 ? name : Path.Combine(parentDir, name); + return new FuzzOp.CreateDir(relativePath); + } + + // Create a file in a random directory + var dir = dirList[random.Next(dirList.Length)]; + var fileIndex = workspace.Files.Count; + return random.Next(4) switch + { + 0 => new FuzzOp.WriteFbaFile(Path.Combine(dir, $"Fba{fileIndex}.cs")), + 1 => new FuzzOp.WriteOrdinaryCs(Path.Combine(dir, $"Ordinary{fileIndex}.cs")), + 2 => new FuzzOp.WriteCsproj(Path.Combine(dir, $"Project{fileIndex}.csproj")), + _ => new FuzzOp.WriteFbaFile(Path.Combine(dir, $"Fba{fileIndex}.cs")), + }; + } + + /// + /// Generates a random "edit" operation (modifying, deleting, renaming files, or creating/deleting csproj). + /// + private static FuzzOp? GenerateEditOp(Random random, FuzzWorkspace workspace) + { + var files = workspace.Files.ToArray(); + if (files.Length == 0) + return null; + + var dirList = workspace.Directories.ToArray(); + var choice = random.Next(7); + + if (choice == 0) + return new FuzzOp.DeleteFile(files[random.Next(files.Length)]); + + if (choice == 1) + { + var oldPath = files[random.Next(files.Length)]; + var dir = dirList[random.Next(dirList.Length)]; + var newPath = Path.Combine(dir, "moved_" + Path.GetFileName(oldPath)); + if (workspace.Files.Contains(newPath)) + return null; + return new FuzzOp.RenameFile(oldPath, newPath); + } + + if (choice == 2) + return new FuzzOp.WriteFbaFile(Path.Combine(dirList[random.Next(dirList.Length)], $"NewFba{workspace.Files.Count + random.Next(100)}.cs")); + + if (choice == 3) + return new FuzzOp.WriteOrdinaryCs(Path.Combine(dirList[random.Next(dirList.Length)], $"NewOrd{workspace.Files.Count + random.Next(100)}.cs")); + + if (choice == 4) + return new FuzzOp.WriteCsproj(Path.Combine(dirList[random.Next(dirList.Length)], $"New{workspace.Files.Count + random.Next(100)}.csproj")); + + var csFiles = files.Where(f => f.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)).ToArray(); + if (csFiles.Length == 0) + return null; + + if (choice == 5) + return new FuzzOp.WriteFbaFile(csFiles[random.Next(csFiles.Length)]); + + if (choice == 6) + return new FuzzOp.WriteOrdinaryCs(csFiles[random.Next(csFiles.Length)]); + + throw ExceptionUtilities.UnexpectedValue(choice); + } + + [Fact] + public async Task Fuzz() + { + // Explicitly seed the random so that if we need to manually edit and repro the fuzzing process locally, the logs will help us to do that + var seed = Random.Shared.Next(); + var random = new Random(seed); + _testOutputHelper.WriteLine($"Random seed: {seed}"); + + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace1" } + ] + }); + var discovery = testLspServer.GetRequiredLspService(); + + for (var iteration = 0; iteration < 1000; iteration++) + { + var workspace = new FuzzWorkspace(tempDir.Path); + var setupOps = new List(); + var editOps = new List(); + + try + { + // Clean workspace for each iteration + foreach (var entry in Directory.EnumerateFileSystemEntries(tempDir.Path)) + { + if (File.Exists(entry)) + File.Delete(entry); + else if (Directory.Exists(entry)) + Directory.Delete(entry, recursive: true); + } + + // Delete cache from any prior iteration + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path); + if (Directory.Exists(cacheDirectory)) + Directory.Delete(cacheDirectory, recursive: true); + + // Step 1: Generate random initial filesystem + var setupCount = random.Next(3, 12); + for (var i = 0; i < setupCount; i++) + { + var op = GenerateSetupOp(random, workspace); + setupOps.Add(op); + workspace.Apply(op); + } + + // Step 2: Discover entry points without cache + var firstResult = discovery.FindEntryPoints(tempDir.Path).ToArray(); + + // Step 3: Random edits + var editCount = random.Next(1, 8); + for (var i = 0; i < editCount; i++) + { + var op = GenerateEditOp(random, workspace); + if (op != null) + { + editOps.Add(op); + workspace.Apply(op); + } + } + + // Step 4: Discover entry points using cache (cache was written by step 2) + var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + // Step 5: Delete the cache + if (Directory.Exists(cacheDirectory)) + Directory.Delete(cacheDirectory, recursive: true); + + // Step 6: Discover without cache — should match step 4 + var uncachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + AssertEx.SequenceEqual(uncachedResult, cachedResult, StringComparer.OrdinalIgnoreCase, + $"Iteration {iteration}: Cached result differs from uncached result."); + } + catch (Exception ex) when (IOUtilities.IsNormalIOException(ex)) + { + // Directories can randomly fail to delete etc when we are thrashing the disk. + // Not a big deal and not a reason to fail the test, just move on to the next iteration instead. + _testOutputHelper.WriteLine($"IO exception during fuzz testing: {ex.Message}"); + } + catch (Exception) + { + // Dump reproducible test case + DumpFuzzReproCase(iteration, setupOps, editOps); + throw; + } + } + } + + private void DumpFuzzReproCase(int iteration, List setupOps, List editOps) + { + var sb = new System.Text.StringBuilder(); + sb.AppendLine($$""" + + [Fact] + public async Task Fuzz_{{iteration}}() + { + var tempDir = _tempRoot.CreateDirectory(); + DeferDeleteCacheDirectory(tempDir.Path); + sb.AppendLine(); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace: false, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = \"workspace1\" } + ] + }); + var discovery = testLspServer.GetRequiredLspService(); + sb.AppendLine(); + + // Setup + """); + foreach (var op in setupOps) + sb.AppendLine($" {op.ToCSharp("tempDir")}"); + + sb.AppendLine(""" + + // First discovery (no cache) + var firstResult = discovery.FindEntryPoints(tempDir.Path).ToArray(); + + // Edits + """); + foreach (var op in editOps) + sb.AppendLine($" {op.ToCSharp("tempDir")}"); + + sb.AppendLine(""" + + // Discovery with cache + var cachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + + // Delete cache + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(tempDir.Path); + Directory.Delete(cacheDirectory, recursive: true); + + // Discovery without cache — should match + var uncachedResult = discovery.FindEntryPoints(tempDir.Path).Order(StringComparer.OrdinalIgnoreCase).ToArray(); + AssertEx.SequenceEqual(uncachedResult, cachedResult, StringComparer.OrdinalIgnoreCase); + } + """); + + _testOutputHelper.WriteLine(sb.ToString()); + } + + #endregion +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs index ba62a390b745..bb4df0891457 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs @@ -2,16 +2,23 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Immutable; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Miscellaneous; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.UnitTests; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; +using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; +using Roslyn.Utilities; +using StreamJsonRpc; using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; @@ -71,6 +78,198 @@ private protected override Workspace GetHostWorkspace(TestLspServer testLspServe return workspaceFactory.HostWorkspace; } + [Theory, CombinatorialData] + public async Task TestFileBasedProgram_Simple(bool mutatingLspWorkspace) + { + // Simple case where document is classified as file-based program and virtual project is loaded. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var tempDir = _tempRoot.CreateDirectory(); + var sourceText = """ + #:sdk Microsoft.Net.Sdk + Console.WriteLine("Hello World!"); + """; + var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(sourceText); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUri, sourceText).ConfigureAwait(false); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.False(document.Project.State.HasAllInformation); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + + // Diagnostics not reported for '#:' + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None)); + + await WaitForProjectLoad(looseFileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + + // Diagnostics not reported for '#:' + syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None)); + } + + [Theory, CombinatorialData] + public async Task TestFileBasedProgram_Extensionless(bool mutatingLspWorkspace) + { + // Unix utility case. Users want to mark C# files as executable, remove the '.cs' extension and use them in the shell, like any other unix CLI utility. + // Users should be able to get full editor support for such files, as long as they set the correct language mode. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var tempDir = _tempRoot.CreateDirectory(); + var sourceText = """ + #!/usr/bin/env dotnet + #:sdk Microsoft.Net.Sdk + Console.WriteLine("Hello World!"); + """; + var sourceFile = tempDir.CreateFile("greeter").WriteAllText(sourceText); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUri, sourceText, languageId: "csharp").ConfigureAwait(false); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.False(document.Project.State.HasAllInformation); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + + // Diagnostics not reported for '#:'/'#!' + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None)); + + await WaitForProjectLoad(looseFileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + + // Diagnostics not reported for '#:'/'#!' + syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None)); + } + + [Theory, CombinatorialData] + public async Task TestFileBasedProgram_Multitargeting(bool mutatingLspWorkspace) + { + // Load a file-based app which multitargets via `#:property TargetFrameworks`. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var tempDir = _tempRoot.CreateDirectory(); + var sourceText = """ + #:property TargetFrameworks=net8.0;net10.0 + Console.WriteLine("Hello World!"); + """; + var sourceFile = tempDir.CreateFile("greeter").WriteAllText(sourceText); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUri, sourceText, languageId: "csharp").ConfigureAwait(false); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + await WaitForProjectLoad(looseFileUri, testLspServer); + + // Just ensure that we got some fully formed document which has all the information for one of the targets. + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + } + + [Theory, CombinatorialData] + public async Task TestFileBasedProgram_EntryPointClosed(bool mutatingLspWorkspace) + { + // Show that a file-based program project is unloaded when the entry point file is closed. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var tempDir = _tempRoot.CreateDirectory(); + var sourceText = """ + #:sdk Microsoft.Net.Sdk + Console.WriteLine("Hello World!"); + """; + var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(sourceText); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUri, sourceText).ConfigureAwait(false); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + // Document is in misc workspace in a primordial state before project load completes. + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + + await testLspServer.CloseDocumentAsync(looseFileUri); + + (workspace, document) = await GetLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Null(workspace); + Assert.Null(document); + } + + [Theory, CombinatorialData] + public async Task TestFileBasedProgram_EntryPointClosed_RemainsLoadedWhenDiscoveryEnabled(bool mutatingLspWorkspace) + { + // When automatic discovery is enabled, closing the entry point file should not unload the project. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var tempDir = _tempRoot.CreateDirectory(); + var sourceText = """ + #:sdk Microsoft.Net.Sdk + Console.WriteLine("Hello World!"); + """; + var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(sourceText); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUri, sourceText).ConfigureAwait(false); + await WaitForProjectLoad(looseFileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + await testLspServer.CloseDocumentAsync(looseFileUri); + + // Project remains loaded because automatic discovery is enabled (default). + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData] + public async Task TestFileBasedProgram_EntryPointClosed_UnloadedWhenDiscoveryDisabled(bool mutatingLspWorkspace) + { + // When automatic discovery is disabled, closing the entry point file should unload the project. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + OptionUpdater = options => options.SetGlobalOption(FileBasedAppsOptionsStorage.EnableAutomaticDiscovery, false), + }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var tempDir = _tempRoot.CreateDirectory(); + var sourceText = """ + #:sdk Microsoft.Net.Sdk + Console.WriteLine("Hello World!"); + """; + var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(sourceText); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUri, sourceText).ConfigureAwait(false); + await WaitForProjectLoad(looseFileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + await testLspServer.CloseDocumentAsync(looseFileUri); + + // Project is unloaded because automatic discovery is disabled. + (workspace, document) = await GetLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Null(workspace); + Assert.Null(document); + } + [Theory, CombinatorialData] public async Task TestLooseFilesInCanonicalProject(bool mutatingLspWorkspace) { @@ -78,7 +277,7 @@ public async Task TestLooseFilesInCanonicalProject(bool mutatingLspWorkspace) await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); - var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); + var looseFileUriOne = CreateAbsoluteDocumentUri("SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUriOne, """ class A { @@ -88,17 +287,16 @@ void M() } """).ConfigureAwait(false); - // File should be initially added as a primordial document in the canonical misc files project with no metadata references. + // Document should be initially found in a primordial misc files project var (_, looseDocumentOne) = await GetLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); Assert.NotNull(looseDocumentOne); - // Should have the primordial canonical document and the loose document. - Assert.Equal(2, looseDocumentOne.Project.Documents.Count()); + Assert.Equal(1, looseDocumentOne.Project.Documents.Count()); Assert.Empty(looseDocumentOne.Project.MetadataReferences); // Wait for the canonical project to finish loading. await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); - // Verify the document is loaded in the canonical project. + // Verify the document is found in a forked canonical project. var (_, canonicalDocumentOne) = await GetLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); Assert.NotNull(canonicalDocumentOne); Assert.NotEqual(looseDocumentOne, canonicalDocumentOne); @@ -117,6 +315,11 @@ void OtherMethod() """).ConfigureAwait(false); var (_, canonicalDocumentTwo) = await GetLspWorkspaceAndDocumentAsync(looseFileUriTwo, testLspServer).ConfigureAwait(false); + + // Wait for the canonical project to finish loading. + await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + (_, canonicalDocumentTwo) = await GetLspWorkspaceAndDocumentAsync(looseFileUriTwo, testLspServer).ConfigureAwait(false); + Assert.NotNull(canonicalDocumentTwo); Assert.NotEqual(canonicalDocumentOne.Project.Id, canonicalDocumentTwo.Project.Id); Assert.DoesNotContain(canonicalDocumentTwo.Project.Documents, d => d.Name == looseDocumentOne.Name); @@ -139,10 +342,9 @@ await testLspServer.OpenDocumentAsync(nonFileUri, """ Console.WriteLine("Hello World"); """).ConfigureAwait(false); - // File should be initially added as a primordial document in the canonical misc files project with no metadata references. + // File should be initially found in a primordial misc files project var (_, primordialDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false); - // Should have the primordial canonical document and the loose document. - Assert.Equal(2, primordialDocument.Project.Documents.Count()); + Assert.Equal(1, primordialDocument.Project.Documents.Count()); Assert.Empty(primordialDocument.Project.MetadataReferences); // No errors for '#:' are expected. @@ -165,42 +367,38 @@ await testLspServer.OpenDocumentAsync(nonFileUri, """ Assert.Empty(canonicalSyntaxTree.GetDiagnostics(CancellationToken.None)); } - [Theory, CombinatorialData] + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81644")] public async Task TestScriptsWithIgnoredDirectives(bool mutatingLspWorkspace) { - // https://github.com/dotnet/roslyn/issues/81644: A csx script with '#:' directives should not be loaded as a file-based program await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); - var nonFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\script.csx"); + var nonFileUri = CreateAbsoluteDocumentUri("script.csx"); await testLspServer.OpenDocumentAsync(nonFileUri, """ #:sdk Microsoft.Net.Sdk Console.WriteLine("Hello World"); """).ConfigureAwait(false); - // File is added to a miscellaneous project containing only the script. - var (_, primordialDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false); - Assert.Equal(1, primordialDocument.Project.Documents.Count()); - Assert.Empty(primordialDocument.Project.MetadataReferences); - - // FileBasedProgram feature flag is not passed, so an error is expected on '#:'. - var primordialSyntaxTree = await primordialDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None); - primordialSyntaxTree.GetDiagnostics(CancellationToken.None).Verify( - // script.csx(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')" - // #:sdk Microsoft.Net.Sdk - TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2)); + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false); + await verifyAsync(workspace, document); - // Wait for project load to finish await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); - var (miscWorkspace, canonicalDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false); - Assert.Equal(WorkspaceKind.Host, miscWorkspace.Kind); - Assert.NotNull(canonicalDocument); - Assert.NotEqual(primordialDocument, canonicalDocument); - Assert.Contains(canonicalDocument.Project.Documents, d => d.Name == "script.AssemblyInfo.cs"); + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false); + await verifyAsync(workspace, document); - var canonicalSyntaxTree = await canonicalDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None); - Assert.Empty(canonicalSyntaxTree.GetDiagnostics(CancellationToken.None)); + async Task verifyAsync(Workspace workspace, Document document) + { + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.Equal(1, document.Project.Documents.Count()); + Assert.Empty(document.Project.MetadataReferences); + + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + syntaxTree.GetDiagnostics(CancellationToken.None).Verify( + // script.csx(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')" + // #:sdk Microsoft.Net.Sdk + TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2)); + } } [Theory, CombinatorialData] @@ -210,16 +408,18 @@ public async Task TestSemanticDiagnosticsEnabledWhenTopLevelStatementsAdded(bool await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); - var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); - await testLspServer.OpenDocumentAsync(looseFileUriOne, """ + var tempDir = _tempRoot.CreateDirectory(); + var initialText = """ class C { } - """).ConfigureAwait(false); + """; + var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(initialText); + var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUriOne, initialText).ConfigureAwait(false); - // File should be initially added as a primordial document in the canonical misc files project with no metadata references. + // File should be initially found in a primordial misc files project var (miscFilesWorkspace, looseDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind); - // Should have the primordial canonical document and the loose document. - Assert.Equal(2, looseDocumentOne.Project.Documents.Count()); + Assert.Equal(1, looseDocumentOne.Project.Documents.Count()); Assert.Empty(looseDocumentOne.Project.MetadataReferences); // Semantic diagnostics are not expected because we haven't loaded references Assert.False(looseDocumentOne.Project.State.HasAllInformation); @@ -237,7 +437,11 @@ class C { } // Adding a top-level statement to a misc file causes it to report semantic errors. var textToInsert = $"""Console.WriteLine("Hello World!");{Environment.NewLine}"""; + // Write updated content to disk so the project system can pick up the change. + sourceFile.WriteAllText($"{textToInsert}{initialText}"); await testLspServer.InsertTextAsync(looseFileUriOne, (Line: 0, Column: 0, Text: textToInsert)); + await Task.Delay(100); + await WaitForProjectLoad(looseFileUriOne, testLspServer); var (workspace, canonicalDocumentTwo) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); Assert.Equal(""" Console.WriteLine("Hello World!"); @@ -245,8 +449,6 @@ class C { } """, (await canonicalDocumentTwo.GetSyntaxRootAsync())!.ToFullString()); Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); - // When presence of top-level statements changes, the misc project is forked again in order to change attributes. - Assert.NotEqual(canonicalDocumentOne.Project.Id, canonicalDocumentTwo.Project.Id); // Now that it has top-level statements, it should be considered to have all information. Assert.True(canonicalDocumentTwo.Project.State.HasAllInformation); } @@ -262,7 +464,7 @@ public async Task TestSemanticDiagnosticsNotEnabledWhenCoarseGrainedFlagDisabled }); Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); - var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); + var looseFileUriOne = CreateAbsoluteDocumentUri("SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUriOne, """ #:sdk Microsoft.Net.Sdk Console.WriteLine("Hello World!"); @@ -272,26 +474,29 @@ class C { } // File should be a "primordial" miscellaneous document and stay that way. var (miscFilesWorkspace, looseDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind); - verify(looseDocumentOne); - - Assert.Single(looseDocumentOne.Project.Documents); - Assert.Empty(looseDocumentOne.Project.MetadataReferences); - // Semantic diagnostics are not expected because we haven't loaded references - Assert.False(looseDocumentOne.Project.State.HasAllInformation); + await verifyAsync(looseDocumentOne); // Wait for project initialization to complete await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); // Document is still in a primordial miscellaneous project var (_, looseDocumentTwo) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); - verify(looseDocumentTwo); + await verifyAsync(looseDocumentTwo); - void verify(Document looseDocument) + async Task verifyAsync(Document looseDocument) { Assert.Single(looseDocument.Project.Documents); Assert.Empty(looseDocument.Project.MetadataReferences); // Semantic diagnostics are not expected because we haven't loaded references Assert.False(looseDocument.Project.State.HasAllInformation); + Assert.DoesNotContain("FileBasedProgram", looseDocument.Project.ParseOptions!.Features); + + // FileBasedProgram feature flag is not passed, so an error is expected on '#:'. + var primordialSyntaxTree = await looseDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None); + primordialSyntaxTree.GetDiagnostics(CancellationToken.None).Verify( + // C:\SomeFile.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')" + // #:sdk Microsoft.Net.Sdk + TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2)); } } @@ -302,21 +507,20 @@ public async Task TestSemanticDiagnosticsNotEnabledWhenFineGrainedFlagDisabled(b await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, - OptionUpdater = options => options.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedProgramsWhenAmbiguous, false) + OptionUpdater = options => options.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableSemanticErrorsInMiscellaneousFiles, false) }); Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); - var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); + var looseFileUriOne = CreateAbsoluteDocumentUri("SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUriOne, """ Console.WriteLine("Hello World!"); class C { } """).ConfigureAwait(false); - // File should be initially added as a primordial document in the canonical misc files project with no metadata references. + // File should be initially found in a primordial misc files project var (miscFilesWorkspace, looseDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind); - // Should have the primordial canonical document and the loose document. - Assert.Equal(2, looseDocumentOne.Project.Documents.Count()); + Assert.Equal(1, looseDocumentOne.Project.Documents.Count()); Assert.Empty(looseDocumentOne.Project.MetadataReferences); // Semantic diagnostics are not expected because we haven't loaded references Assert.False(looseDocumentOne.Project.State.HasAllInformation); @@ -333,6 +537,181 @@ class C { } Assert.False(canonicalDocumentOne.Project.State.HasAllInformation); } + [Theory, CombinatorialData] + public async Task TestEnableFileBasedProgramsChangedDynamically_01(bool mutatingLspWorkspace) + { + // Toggle the EnableFileBasedPrograms setting while file-based program project is not fully loaded. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var tempDir = _tempRoot.CreateDirectory(); + var sourceText = """ + #:sdk Microsoft.Net.Sdk + Console.WriteLine("Hello World!"); + """; + var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(sourceText); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUri, sourceText).ConfigureAwait(false); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.Equal(1, document.Project.Documents.Count()); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None)); + + var globalOptions = testLspServer.TestWorkspace.ExportProvider.GetExportedValue(); + globalOptions.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, false); + + (_, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.DoesNotContain("FileBasedProgram", document.Project.ParseOptions!.Features); + syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + syntaxTree.GetDiagnostics(CancellationToken.None).Verify( + // C:\SomeFile.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')" + // #:sdk Microsoft.Net.Sdk + TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2)); + + globalOptions.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, true); + + (_, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + + syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None)); + } + + private async ValueTask WaitForProjectLoad(DocumentUri looseFileUri, TestLspServer testLspServer) + { + _ = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + } + + [Theory, CombinatorialData] + public async Task TestEnableFileBasedProgramsChangedDynamically_02(bool mutatingLspWorkspace) + { + // Toggle the EnableFileBasedPrograms setting after a file-based program project is fully loaded. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var tempDir = _tempRoot.CreateDirectory(); + var sourceText = """ + #:sdk Microsoft.Net.Sdk + Console.WriteLine("Hello World!"); + """; + var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(sourceText); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUri, sourceText).ConfigureAwait(false); + + await WaitForProjectLoad(looseFileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + Assert.True(document.Project.State.HasAllInformation); + + var globalOptions = testLspServer.TestWorkspace.ExportProvider.GetExportedValue(); + globalOptions.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, false); + await WaitForProjectLoad(looseFileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.DoesNotContain("FileBasedProgram", document.Project.ParseOptions!.Features); + Assert.False(document.Project.State.HasAllInformation); + + globalOptions.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, true); + await WaitForProjectLoad(looseFileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + Assert.True(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData] + public async Task TestEnableFileBasedProgramsChangedDynamically_03(bool mutatingLspWorkspace) + { + // Toggle the EnableFileBasedPrograms setting after the canonical misc files project is fully loaded. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var tempDir = _tempRoot.CreateDirectory(); + var sourceText = """ + Console.WriteLine("Hello World!"); + """; + var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(sourceText); + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUri, sourceText).ConfigureAwait(false); + + await WaitForProjectLoad(looseFileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + Assert.True(document.Project.State.HasAllInformation); + + var globalOptions = testLspServer.TestWorkspace.ExportProvider.GetExportedValue(); + globalOptions.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, false); + await WaitForProjectLoad(looseFileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.DoesNotContain("FileBasedProgram", document.Project.ParseOptions!.Features); + Assert.False(document.Project.State.HasAllInformation); + + globalOptions.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, true); + await WaitForProjectLoad(looseFileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.Contains("FileBasedProgram", document.Project.ParseOptions!.Features); + Assert.True(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData] + public async Task TestEnableFileBasedProgramsChangedDynamically_Script(bool mutatingLspWorkspace) + { + // Test that scripts are never file based programs, even when changing the setting while running + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + var looseFileUri = CreateAbsoluteDocumentUri("SomeFile.csx"); + await testLspServer.OpenDocumentAsync(looseFileUri, """ + #:sdk Microsoft.Net.Sdk + Console.WriteLine("Hello World!"); + """).ConfigureAwait(false); + await WaitForProjectLoad(looseFileUri, testLspServer); + + var (_, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + await verifyAsync(document); + + var globalOptions = testLspServer.TestWorkspace.ExportProvider.GetExportedValue(); + globalOptions.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, false); + await WaitForProjectLoad(looseFileUri, testLspServer); + + (_, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + await verifyAsync(document); + + globalOptions.SetGlobalOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, true); + await WaitForProjectLoad(looseFileUri, testLspServer); + + (_, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false); + await verifyAsync(document); + + async ValueTask verifyAsync(Document document) + { + Assert.Equal(WorkspaceKind.MiscellaneousFiles, document.Project.Solution.WorkspaceKind); + Assert.Equal(1, document.Project.Documents.Count()); + Assert.DoesNotContain("FileBasedProgram", document.Project.ParseOptions!.Features); + + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + syntaxTree.GetDiagnostics(CancellationToken.None).Verify( + // C:\SomeFile.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')" + // #:sdk Microsoft.Net.Sdk + TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2)); + } + } + [Theory, CombinatorialData] public async Task TestFileBecomesFileBasedProgramWhenDirectiveAdded(bool mutatingLspWorkspace) { @@ -340,60 +719,757 @@ public async Task TestFileBecomesFileBasedProgramWhenDirectiveAdded(bool mutatin await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); - var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); - await testLspServer.OpenDocumentAsync(looseFileUriOne, """ + var tempDir = _tempRoot.CreateDirectory(); + var initialText = """ Console.WriteLine("Hello World!"); - """).ConfigureAwait(false); + """; + var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(initialText); + var looseFileUriOne = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path); + await testLspServer.OpenDocumentAsync(looseFileUriOne, initialText).ConfigureAwait(false); - // File should be initially added as a primordial document in the canonical misc files project with no metadata references. - var (miscFilesWorkspace, looseDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); - Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind); - // Should have the primordial canonical document and the loose document. - Assert.Equal(2, looseDocumentOne.Project.Documents.Count()); - Assert.Empty(looseDocumentOne.Project.MetadataReferences); + // File should be initially found in a primordial misc files project + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.Equal(1, document.Project.Documents.Count()); + Assert.Empty(document.Project.MetadataReferences); // Semantic diagnostics are not expected because we haven't loaded references - Assert.False(looseDocumentOne.Project.State.HasAllInformation); + Assert.False(document.Project.State.HasAllInformation); // Wait for the canonical project to finish loading. await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); // Verify the document is loaded in the canonical project. - (miscFilesWorkspace, var canonicalDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); - Assert.NotEqual(looseDocumentOne, canonicalDocumentOne); - // Should have the appropriate generated files now that we ran a design time build - Assert.Contains(canonicalDocumentOne.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs"); + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); // This is not loaded as a file-based program (no dedicated restore done for it etc.), so it should be in the misc workspace. - Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscFilesWorkspace.Kind); - // Because we have top-level statements, it should be considered to have all information (semantic diagnostics should be reported etc.) - Assert.True(canonicalDocumentOne.Project.State.HasAllInformation); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.Contains(document.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs"); + Assert.True(document.Project.State.HasAllInformation); // Adding a #! directive to a misc file causes it to move to a file-based program project. var textToInsert = $"#!/usr/bin/env dotnet{Environment.NewLine}"; + // Write updated content to disk so the build host can load it. + sourceFile.WriteAllText($"#!/usr/bin/env dotnet{Environment.NewLine}{initialText}"); await testLspServer.InsertTextAsync(looseFileUriOne, (Line: 0, Column: 0, Text: textToInsert)); - var (_, fileBasedDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); - // The document is now in a primordial state in the FileBasedProgramsProjectSystem. - Assert.NotEqual(fileBasedDocumentOne, canonicalDocumentOne); - var fileBasedProject = fileBasedDocumentOne.Project; - Assert.Same(miscFilesWorkspace, fileBasedProject.Solution.Workspace); - Assert.NotEqual(canonicalDocumentOne.Project.Id, fileBasedProject.Id); - Assert.Equal(""" - #!/usr/bin/env dotnet - Console.WriteLine("Hello World!"); - """, - (await fileBasedDocumentOne.GetSyntaxRootAsync())!.ToFullString()); + // Still in canonical project until next DTB finished. + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.Contains(document.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs"); + Assert.True(document.Project.State.HasAllInformation); // Verify that the project system remains in a good state, when intermediate requests come in while the file-based program project is still loaded. - var (_, alsoFileBasedDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); - Assert.Equal(fileBasedProject.Id, alsoFileBasedDocumentOne.Project.Id); + var (_, document2) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); + Assert.Equal(document.Project.Id, document2.Project.Id); // Wait for the file-based program project to load. await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); - var (hostWorkspace, fullFileBasedDocumentOne) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); - Assert.Equal(WorkspaceKind.Host, hostWorkspace!.Kind); - Assert.NotEqual(fileBasedProject.Id, fullFileBasedDocumentOne!.Project.Id); - Assert.Contains(fullFileBasedDocumentOne!.Project.Documents, d => d.Name == "SomeFile.AssemblyInfo.cs"); - // Because it is loaded as a file-based program, it should be considered to have all information (semantic diagnostics should be reported etc.) - Assert.True(canonicalDocumentOne.Project.State.HasAllInformation); + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUriOne, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.Contains(document.Project.Documents, d => d.Name == "SomeFile.AssemblyInfo.cs"); + Assert.True(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData] + public async Task TestFileStopsBeingFileBasedProgramWhenDirectivesDeleted(bool mutatingLspWorkspace) + { + var tempDir = _tempRoot.CreateDirectory(); + var appCsText = """ + #!/usr/bin/env dotnet + Console.WriteLine("Hello World!"); + """; + var appCsFile = tempDir.CreateFile("App.cs").WriteAllText(appCsText); + + await using var testLspServer = await CreateTestLspServerAsync( + string.Empty, + mutatingLspWorkspace, + new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var appCsUri = CreateAbsoluteDocumentUri(appCsFile.Path); + await testLspServer.OpenDocumentAsync(appCsUri, """ + #!/usr/bin/env dotnet + Console.WriteLine("Hello World!"); + """).ConfigureAwait(false); + await WaitForProjectLoad(appCsUri, testLspServer); + + // Document is loaded as a file-based app + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(appCsUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + // Removing a #! directive (line 0) causes it to stop being a file-based app + var newAppCsText = """ + Console.WriteLine("Hello World!"); + """; + await testLspServer.DeleteTextAsync(appCsUri, + (StartLine: 0, StartColumn: 0, EndLine: 1, EndColumn: 0)); + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(appCsUri, testLspServer).ConfigureAwait(false); + Assert.Equal(newAppCsText, (await document.GetTextAsync()).ToString()); + + // Flush the document change to disk to trigger a reload of the FBA project. + appCsFile.WriteAllText(newAppCsText); + // Wait for the batching queue timeout. + await Task.Delay(100); + await WaitForProjectLoad(appCsUri, testLspServer); + + // Now the document is a miscellaneous file + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(appCsUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestDiagnosticsRequestedAfterDocumentClosed(bool mutatingLspWorkspace) + { + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var nonFileUri = ProtocolConversions.CreateAbsoluteDocumentUri("untitled:Untitled-1"); + await testLspServer.OpenDocumentAsync(nonFileUri, """ + Console.WriteLine("Hello World"); + """, languageId: "csharp").ConfigureAwait(false); + + // Get the document info once to kickoff the canonical project loading process + _ = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false); + await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + + // Verify the document is loaded in the canonical project. + var (miscWorkspace, canonicalDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, miscWorkspace.Kind); + Assert.NotNull(canonicalDocument); + // Should have the appropriate generated files now that we ran a design time build + Assert.Contains(canonicalDocument.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs"); + + // File was saved to disk. Simulate this by opening the document under its new name and closing it under its old name. + var fileUri = CreateAbsoluteDocumentUri("MyFile.cs"); + await testLspServer.OpenDocumentAsync(fileUri, """ + Console.WriteLine("Hello World"); + """).ConfigureAwait(false); + + await testLspServer.CloseDocumentAsync(nonFileUri).ConfigureAwait(false); + + // Issue a "textDocument/diagnostic" request for the closed document + var exception = await Assert.ThrowsAsync(() => + testLspServer.ExecuteRequestAsync>( + Methods.TextDocumentDiagnosticName, + new DocumentDiagnosticParams() { TextDocument = new TextDocumentIdentifier { DocumentUri = nonFileUri } }, + CancellationToken.None)); + Assert.Equal(RoslynLspErrorCodes.NonFatalRequestFailure, exception.ErrorCode); + + // Issue a "textDocument/hover" request for the closed document + // At the time of authoring this test, the HoverHandler calls 'GetRequiredDocument'. + // Demonstrate that instead of calling the handler (which would fail, due to the request not having any document), + // we throw an exception with a known error code. + exception = await Assert.ThrowsAsync(() => + testLspServer.ExecuteRequestAsync( + Methods.TextDocumentHoverName, + new HoverParams() { Position = new Position(0, 0), TextDocument = new TextDocumentIdentifier { DocumentUri = nonFileUri } }, + CancellationToken.None)); + Assert.Equal(RoslynLspErrorCodes.NonFatalRequestFailure, exception.ErrorCode); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestMultiFile_Simulated_01(bool mutatingLspWorkspace) + { + // Use a Directory.Build.props in the same directory to simulate an '#:include' directive. + var tempDir = _tempRoot.CreateDirectory(); + var dbPropsText = """ + + + + + + """; + var dbPropsFile = tempDir.CreateFile("Directory.Build.props").WriteAllText(dbPropsText); + + var utilCsText = """ + internal class Util { } + """; + var utilCsFile = tempDir.CreateFile("Util.cs").WriteAllText(utilCsText); + + var appCsText = """ + #:property A=B + new Util(); + """; + var appCsFile = tempDir.CreateFile("App.cs").WriteAllText(appCsText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var appCsUri = ProtocolConversions.CreateAbsoluteDocumentUri(appCsFile.Path); + await testLspServer.OpenDocumentAsync(appCsUri, appCsText).ConfigureAwait(false); + await WaitForProjectLoad(appCsUri, testLspServer); + + // Verify no semantic errors for App.cs + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(appCsUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + var model = await document.GetRequiredSemanticModelAsync(CancellationToken.None); + Assert.Empty(model.GetDiagnostics()); + + // Verify no semantic errors for Util.cs + var appCsProject = document.Project; + var utilCsUri = ProtocolConversions.CreateAbsoluteDocumentUri(utilCsFile.Path); + + // app.cs and util.cs are part of the same project + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(utilCsUri, testLspServer).ConfigureAwait(false); + Assert.True(appCsProject.Id.Equals(document.Project.Id), + $"Unexpected false: ({appCsProject.FilePath}, {appCsProject.Id}), != ({document.Project.FilePath}, {document.Project.Id})"); + + model = await document.GetRequiredSemanticModelAsync(CancellationToken.None); + Assert.Empty(model.GetDiagnostics()); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestMultiFile_Simulated_02(bool mutatingLspWorkspace) + { + // Reference the same non-entry-point file in both an `#:include` and ` + + + + + """; + var dbPropsFile = tempDir.CreateFile("Directory.Build.props").WriteAllText(dbPropsText); + + var utilCsText = """ + internal class Util { } + """; + var utilCsFile = tempDir.CreateFile("Util.cs").WriteAllText(utilCsText); + + var appCsText = """ + #:property A=B + new Util(); + """; + var appCsFile = tempDir.CreateFile("App.cs").WriteAllText(appCsText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var appCsUri = ProtocolConversions.CreateAbsoluteDocumentUri(appCsFile.Path); + await testLspServer.OpenDocumentAsync(appCsUri, appCsText).ConfigureAwait(false); + await WaitForProjectLoad(appCsUri, testLspServer); + + // app.cs project was loaded and includes util.cs as one of the files + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(appCsUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + Assert.True(document.Project.Documents.Contains(document => document.Name == "Util.cs")); + + // Add an ordinary project containing the same "util.cs" file + workspace.SetCurrentSolution( + solution => solution.AddProject("Ordinary", "Ordinary", LanguageNames.CSharp) + .AddDocument("Util.cs", SourceText.From(utilCsText), filePath: utilCsFile.Path) + .Project.Solution, + WorkspaceChangeKind.ProjectAdded); + var solution = workspace.CurrentSolution; + var projects = solution.Projects.ToImmutableArray(); + Assert.Equal(2, projects.Length); + Assert.Equal("App", projects[0].AssemblyName); + Assert.Equal("Ordinary", projects[1].AssemblyName); + + // Lookup "Util.cs" in the "App" project context + var utilCsUri = ProtocolConversions.CreateAbsoluteDocumentUri(utilCsFile.Path); + (_, _, var textDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(utilCsUri, projects[0].Id), CancellationToken.None); + Assert.NotNull(textDocument); + Assert.Equal("App", textDocument.Project.AssemblyName); + + // Lookup "Util.cs" in the "Ordinary" project context + (_, _, textDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(utilCsUri, projects[1].Id), CancellationToken.None); + Assert.NotNull(textDocument); + Assert.Equal("Ordinary", textDocument.Project.AssemblyName); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestOrdinaryProjectContainsFileBasedAppEntryPoint_01(bool mutatingLspWorkspace) + { + // Error scenario: open a file-based app entry point document, + // then load a project which contains the same document. + var tempDir = _tempRoot.CreateDirectory(); + var appCsText = """ + #:property A=B + Console.WriteLine("Hello"); + """; + var appCsFile = tempDir.CreateFile("App.cs").WriteAllText(appCsText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + // Open the file-based app entry point first + var appCsUri = ProtocolConversions.CreateAbsoluteDocumentUri(appCsFile.Path); + await testLspServer.OpenDocumentAsync(appCsUri, appCsText).ConfigureAwait(false); + await WaitForProjectLoad(appCsUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(appCsUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + // Add an ordinary project containing the same "App.cs" file + workspace.SetCurrentSolution( + solution => solution.AddProject("Ordinary", "Ordinary", LanguageNames.CSharp) + .AddDocument("App.cs", SourceText.From(appCsText), filePath: appCsFile.Path) + .Project.Solution, + WorkspaceChangeKind.ProjectAdded); + var solution = workspace.CurrentSolution; + var projects = solution.Projects.ToImmutableArray(); + Assert.Equal(2, projects.Length); + Assert.Equal("App", projects[0].AssemblyName); + Assert.Equal("Ordinary", projects[1].AssemblyName); + + // Lookup "App.cs" in the "App" project context + var utilCsUri = ProtocolConversions.CreateAbsoluteDocumentUri(appCsFile.Path); + (_, _, var textDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync( + CreateTextDocumentIdentifier(utilCsUri, projects[0].Id), CancellationToken.None); + document = (Document)textDocument!; + Assert.Equal("App", document.Project.AssemblyName); + + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + Assert.Contains("FileBasedProgram", syntaxTree.Options.Features.Keys); + Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None)); + + // Lookup "App.cs" in the "Ordinary" project context + (_, _, textDocument) = await testLspServer.GetManager().GetLspDocumentInfoAsync( + CreateTextDocumentIdentifier(utilCsUri, projects[1].Id), CancellationToken.None); + Assert.NotNull(textDocument); + document = (Document)textDocument!; + Assert.Equal("Ordinary", document.Project.AssemblyName); + + // TODO: it's unclear why, but, a syntax error is not being reported for '#:' here despite absence of FileBasedProgram feature flag + // Perhaps a syntax tree from the other document is being reused? + syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + Assert.DoesNotContain("FileBasedProgram", syntaxTree.Options.Features.Keys); + Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None)); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestOrdinaryProjectContainsFileBasedAppEntryPoint_02(bool mutatingLspWorkspace) + { + // Error scenario: load a project which contains a file-based app entry point, + // then open the file-based app entry point. + var tempDir = _tempRoot.CreateDirectory(); + var appCsText = """ + #:property A=B + Console.WriteLine("Hello"); + """; + var appCsFile = tempDir.CreateFile("App.cs").WriteAllText(appCsText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + // Add an ordinary project containing the "App.cs" file + testLspServer.TestWorkspace.SetCurrentSolution( + solution => solution.AddProject("Ordinary", "Ordinary", LanguageNames.CSharp) + .AddDocument("App.cs", SourceText.From(appCsText), filePath: appCsFile.Path) + .Project.Solution, + WorkspaceChangeKind.ProjectAdded); + + // Document is found in the ordinary project and syntax error reported on '#:' + var appCsUri = ProtocolConversions.CreateAbsoluteDocumentUri(appCsFile.Path); + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(appCsUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.Equal("Ordinary", document.Project.AssemblyName); + + var syntaxTree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None); + syntaxTree.GetDiagnostics(CancellationToken.None).Verify( + // App.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')" + // #:property A=B + TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2)); + + // Now open the file-based app entry point file + await testLspServer.OpenDocumentAsync(appCsUri, appCsText).ConfigureAwait(false); + await WaitForProjectLoad(appCsUri, testLspServer); + + // The file-based app project doesn't end up getting loaded. + // That's OK, as long as something in the editor reports an error (in this case the parse error on '#:' above). + // User needs to either remove App.cs from the ordinary project, or, delete the '#:' directives in App.cs. + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(appCsUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, workspace.Kind); + Assert.Equal("Ordinary", document.Project.AssemblyName); + var assemblyNames = workspace.CurrentSolution.Projects.SelectAsArray(project => project.AssemblyName); + AssertEx.SetEqual(["Test", "Ordinary"], assemblyNames); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestCsprojInCone_01(bool mutatingLspWorkspace) + { + // in-cone: csproj in a containing directory (one level of nesting) + var tempDir = _tempRoot.CreateDirectory(); + var csprojFile = tempDir.CreateFile("Project.csproj"); + + var srcDir = tempDir.CreateDirectory("src"); + var fileText = """ + Console.WriteLine("Hello World"); + """; + var file = srcDir.CreateFile("file.cs").WriteAllText(fileText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = [new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace" }] + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var fileUri = CreateAbsoluteDocumentUri(file.Path); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.False(document.Project.State.HasAllInformation); + + // Check that changes are observed when when closing/reopening the document. + // Note that just deleting/recreating the csproj file doesn't cause us to observe a change, for a document which is already open. + File.Delete(csprojFile.Path); + await WaitForProjectLoad(fileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.False(document.Project.State.HasAllInformation); // We didn't observe the file system change + + await testLspServer.CloseDocumentAsync(fileUri).ConfigureAwait(false); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + csprojFile = tempDir.CreateFile("Project.csproj"); + await WaitForProjectLoad(fileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); // We didn't observe the file system change + + await testLspServer.CloseDocumentAsync(fileUri).ConfigureAwait(false); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.False(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestCsprojInCone_02(bool mutatingLspWorkspace) + { + // in-cone: csproj in same directory + var tempDir = _tempRoot.CreateDirectory(); + var csprojFile = tempDir.CreateFile("Project.csproj"); + + var fileText = """ + Console.WriteLine("Hello World"); + """; + var file = tempDir.CreateFile("file.cs").WriteAllText(fileText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = [new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace" }] + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var fileUri = CreateAbsoluteDocumentUri(file.Path); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.False(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestCsprojInCone_03(bool mutatingLspWorkspace) + { + // in-cone: csproj in a nested containing directory + var tempDir = _tempRoot.CreateDirectory(); + var csprojFile = tempDir.CreateFile("Project.csproj"); + + var src1Dir = tempDir.CreateDirectory("src1"); + var src2Dir = src1Dir.CreateDirectory("src2"); + var fileText = """ + Console.WriteLine("Hello World"); + """; + var file = src2Dir.CreateFile("file.cs").WriteAllText(fileText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = [new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace" }] + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var fileUri = CreateAbsoluteDocumentUri(file.Path); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.False(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestCsprojInCone_04(bool mutatingLspWorkspace) + { + // not-in-cone: csproj in a sibling directory + var tempDir = _tempRoot.CreateDirectory(); + + var src1Dir = tempDir.CreateDirectory("src"); + var csprojFile = src1Dir.CreateFile("Project.csproj"); + + var src2Dir = tempDir.CreateDirectory("src2"); + var fileText = """ + Console.WriteLine("Hello World"); + """; + var file = src2Dir.CreateFile("file.cs").WriteAllText(fileText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = [new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace" }] + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var fileUri = CreateAbsoluteDocumentUri(file.Path); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + // Test that deleting/re-creating an irrelevant csproj doesn't result in a bad project system/workspace behavior. + // i.e. HasAllInformation is unaffected by the irrelevant delete. + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + File.Delete(csprojFile.Path); + await WaitForProjectLoad(fileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + await testLspServer.CloseDocumentAsync(fileUri).ConfigureAwait(false); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + csprojFile = src1Dir.CreateFile("Project.csproj"); + await WaitForProjectLoad(fileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + + await testLspServer.CloseDocumentAsync(fileUri).ConfigureAwait(false); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestCsprojInCone_05(bool mutatingLspWorkspace) + { + // not-in-cone: csproj in a child directory + var tempDir = _tempRoot.CreateDirectory(); + var fileText = """ + Console.WriteLine("Hello World"); + """; + var file = tempDir.CreateFile("file.cs").WriteAllText(fileText); + + var src1Dir = tempDir.CreateDirectory("src1"); + src1Dir.CreateFile("Project.csproj"); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = [new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace" }] + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var fileUri = CreateAbsoluteDocumentUri(file.Path); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestCsprojInCone_06(bool mutatingLspWorkspace) + { + // not-in-cone: csproj in a parent directory above a workspace directory. + // even though the csproj file is "in-cone" in the file system, it's not within the workspace folder, so we act as if it's not-in-cone. + var tempDir = _tempRoot.CreateDirectory(); + var csprojFile = tempDir.CreateFile("Project.csproj"); + + var src1Dir = tempDir.CreateDirectory("src1"); + var fileText = """ + Console.WriteLine("Hello World"); + """; + var file = src1Dir.CreateFile("file.cs").WriteAllText(fileText); + var src2Dir = tempDir.CreateDirectory("src2"); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(src1Dir.Path), Name = "workspace1" }, + new() { DocumentUri = CreateAbsoluteDocumentUri(src2Dir.Path), Name = "workspace2" }, + ] + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var fileUri = CreateAbsoluteDocumentUri(file.Path); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestCsprojInCone_07(bool mutatingLspWorkspace) + { + // in-cone: Test an edge case where multiple workspace folders are in-cone with each other. + // The csproj-in-cone check may do unnecessary work in this case, but, observable behavior must be correct + var tempDir = _tempRoot.CreateDirectory(); + var csprojFile = tempDir.CreateFile("Project.csproj"); + + var src1Dir = tempDir.CreateDirectory("src1"); + var fileText = """ + Console.WriteLine("Hello World"); + """; + var file = src1Dir.CreateFile("file.cs").WriteAllText(fileText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(src1Dir.Path), Name = "workspace1" }, + new() { DocumentUri = CreateAbsoluteDocumentUri(tempDir.Path), Name = "workspace2" }, + ] + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var fileUri = CreateAbsoluteDocumentUri(file.Path); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.False(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestCsprojInCone_08(bool mutatingLspWorkspace) + { + // not-in-cone: No workspace folder is opened at all. Therefore no search is done for a csproj in cone. + var tempDir = _tempRoot.CreateDirectory(); + var csprojFile = tempDir.CreateFile("Project.csproj"); + + var fileText = """ + Console.WriteLine("Hello World"); + """; + var file = tempDir.CreateFile("file.cs").WriteAllText(fileText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var fileUri = CreateAbsoluteDocumentUri(file.Path); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + } + + [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/roslyn/issues/81410")] + public async Task TestCsprojInCone_09(bool mutatingLspWorkspace) + { + // not-in-cone: A workspace folder is open, but a .cs file outside that folder was opened. + // No search is done for a .csproj-in-cone. + var tempDir = _tempRoot.CreateDirectory(); + var src1Dir = tempDir.CreateDirectory("src1"); + var src2Dir = tempDir.CreateDirectory("src2"); + var csprojFile = src2Dir.CreateFile("Project.csproj"); + var fileText = """ + Console.WriteLine("Hello World"); + """; + var file = src2Dir.CreateFile("file.cs").WriteAllText(fileText); + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + { + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + WorkspaceFolders = + [ + new() { DocumentUri = CreateAbsoluteDocumentUri(src1Dir.Path), Name = "workspace1" } + ] + }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var fileUri = CreateAbsoluteDocumentUri(file.Path); + await testLspServer.OpenDocumentAsync(fileUri, fileText).ConfigureAwait(false); + await WaitForProjectLoad(fileUri, testLspServer); + + var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(fileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.MiscellaneousFiles, workspace.Kind); + Assert.True(document.Project.State.HasAllInformation); + } + + /// + /// Test needed to be copy-pasted from the base type in order to properly handle the MEF composition, and meaningfully exercise FileBasedProgramsProjectSystem. + /// + /// + [Theory, CombinatorialData] + public async Task TestLooseFile_RazorFile_FileBasedProgramsProjectSystem(bool mutatingLspWorkspace) + { + // Create a server that supports LSP misc files and verify no misc files present. + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }, composition: null); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + Assert.Null(await GetMiscellaneousAdditionalDocumentAsync(testLspServer)); + + // Open an empty loose file and make a request to verify it gets added to the misc workspace. + var looseFileUri = CreateAbsoluteDocumentUri("SomeFile.razor"); + await testLspServer.OpenDocumentAsync(looseFileUri, "
").ConfigureAwait(false); + + // Trigger a request and assert we got a file in the misc workspace. + await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + Assert.NotNull(await GetMiscellaneousAdditionalDocumentAsync(testLspServer)); + + // Trigger another request and assert we got a file in the misc workspace. + await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); + Assert.NotNull(await GetMiscellaneousAdditionalDocumentAsync(testLspServer)); + + await testLspServer.CloseDocumentAsync(looseFileUri).ConfigureAwait(false); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + Assert.Null(await GetMiscellaneousAdditionalDocumentAsync(testLspServer)); } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj index 651528467e47..7528f15efffe 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj @@ -1,18 +1,13 @@  - $(NetVSCode) + $(NetRoslynWindowsTests) enable enable UnitTest false - - $(NoWarn);NETSDK1206 - diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerDisconnectTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerDisconnectTests.cs new file mode 100644 index 000000000000..b77c53c5e40e --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerDisconnectTests.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public sealed class ServerDisconnectTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerHostTests(testOutputHelper) +{ + [Fact] + public async Task ServerExitsCleanlyOnIOException() + { + var server = await CreateLanguageServerAsync(); + + // Simulate the server getting an EndOfStreamException(IOException) when reading from the JSON-RPC stream. + server.ClientToServerPipe.Writer.Complete(new EndOfStreamException()); + + // Server should exit cleanly without throwing. + await server.ServerExitTask; + } + + [Fact] + public async Task ServerExitsCleanlyWhenClientDisconnects() + { + var server = await CreateLanguageServerAsync(); + + // Simulate the client disconnecting abruptly. + server.ClientToServerPipe.Writer.Complete(); + server.ServerToClientPipe.Reader.Complete(); + + // Server should exit cleanly without throwing. + await server.ServerExitTask; + } + + [Fact] + public async Task ServerThrowsOnStreamCorruption() + { + var server = await CreateLanguageServerAsync(); + + // Write a valid JSON-RPC header with a corrupt (non-JSON) body to cause a deserialization error. + var garbageBody = Encoding.UTF8.GetBytes("this is not valid json!!"); + var header = Encoding.ASCII.GetBytes($"Content-Length: {garbageBody.Length}\r\n\r\n"); + await server.ClientToServerPipe.Writer.WriteAsync(header); + await server.ClientToServerPipe.Writer.WriteAsync(garbageBody); + server.ClientToServerPipe.Writer.Complete(); + + // Corruption is not a clean disconnect - the server should propagate the error. + var exception = await Assert.ThrowsAnyAsync(() => server.ServerExitTask); + Assert.NotNull(exception); + } + + [Fact] + public async Task ServerThrowsOnUnexpectedException() + { + var server = await CreateLanguageServerAsync(); + + server.ClientToServerPipe.Writer.Complete(new InvalidOperationException("Something went wrong")); + + // Unexpected exceptions should propagate to WaitForExitAsync callers. + await Assert.ThrowsAsync(() => server.ServerExitTask); + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerClientTests.TestLspClient.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerClientTests.TestLspClient.cs index 24446d2e13b8..3c0216a23ab4 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerClientTests.TestLspClient.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerClientTests.TestLspClient.cs @@ -41,12 +41,21 @@ internal static async Task CreateAsync( Dictionary>? locations = null) { var pipeName = CreateNewPipeName(); - var processStartInfo = CreateLspStartInfo(pipeName, extensionLogsPath, includeDevKitComponents, debugLsp); + var fullPipePath = GetFullPipePath(pipeName); + + // Create the pipe server - the LSP server process will connect to this as a client. + var pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, maxNumberOfServerInstances: 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + + var processStartInfo = CreateLspStartInfo(fullPipePath, extensionLogsPath, includeDevKitComponents, debugLsp); var process = Process.Start(processStartInfo); Assert.NotNull(process); - var lspClient = new TestLspClient(process, pipeName, documents ?? [], locations ?? [], loggerFactory); + // Wait for the server process to connect to our pipe. + using var cts = new CancellationTokenSource(TimeOutMsNewProcess); + await pipeServer.WaitForConnectionAsync(cts.Token); + + var lspClient = new TestLspClient(process, pipeServer, documents ?? [], locations ?? [], loggerFactory); // We've subscribed to Disconnected, but if the process crashed before that point we might have not seen it if (process.HasExited) @@ -65,15 +74,18 @@ internal static async Task CreateAsync( static string CreateNewPipeName() { - const string WINDOWS_DOTNET_PREFIX = @"\\.\"; - // The pipe name constructed by some systems is very long (due to temp path). // Shorten the unique id for the pipe. var newGuid = Guid.NewGuid().ToString(); - var pipeName = newGuid.Split('-')[0]; + return newGuid.Split('-')[0]; + } + static string GetFullPipePath(string pipeName) + { + // The client creates the pipe server and passes the full pipe path to the LSP server process. + // On Windows this is \\.\pipe\, on Unix it's a socket path. return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? WINDOWS_DOTNET_PREFIX + pipeName + ? @"\\.\pipe\" + pipeName : Path.Combine(Path.GetTempPath(), pipeName + ".sock"); } @@ -123,7 +135,7 @@ static ProcessStartInfo CreateLspStartInfo(string pipeName, string extensionLogs internal ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("Initialize has not been called"); - private TestLspClient(Process process, string pipeName, Dictionary documents, Dictionary> locations, ILoggerFactory loggerFactory) + private TestLspClient(Process process, Stream pipeStream, Dictionary documents, Dictionary> locations, ILoggerFactory loggerFactory) { _documents = documents; _locations = locations; @@ -140,11 +152,8 @@ private TestLspClient(Process process, string pipeName, Dictionary CreateAsync(ClientCapabilities clientCapabilities, ILoggerFactory loggerFactory, string cacheDirectory, bool includeDevKitComponents = true, string[]? extensionPaths = null) { @@ -65,11 +67,18 @@ private TestLspServer(ExportProvider exportProvider, ILoggerFactory loggerFactor { var typeRefResolver = new ExtensionTypeRefResolver(assemblyLoader, loggerFactory); - var (clientStream, serverStream) = FullDuplexStream.CreatePair(); - LanguageServerHost = new LanguageServerHost(serverStream, serverStream, exportProvider, loggerFactory, typeRefResolver); + _clientToServerPipe = new Pipe(); + _serverToClientPipe = new Pipe(); + + var serverInputStream = _clientToServerPipe.Reader.AsStream(); + var serverOutputStream = _serverToClientPipe.Writer.AsStream(); + var clientOutputStream = _clientToServerPipe.Writer.AsStream(); + var clientInputStream = _serverToClientPipe.Reader.AsStream(); + + LanguageServerHost = new LanguageServerHost(serverInputStream, serverOutputStream, exportProvider, loggerFactory, typeRefResolver); var messageFormatter = RoslynLanguageServer.CreateJsonMessageFormatter(); - _clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientStream, clientStream, messageFormatter)) + _clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientOutputStream, clientInputStream, messageFormatter)) { AllowModificationWhileListening = true, ExceptionStrategy = ExceptionProcessing.ISerializable, @@ -85,6 +94,11 @@ private TestLspServer(ExportProvider exportProvider, ILoggerFactory loggerFactor ExportProvider = exportProvider; } + public Task ServerExitTask => _languageServerHostCompletionTask; + + public Pipe ClientToServerPipe => _clientToServerPipe; + public Pipe ServerToClientPipe => _serverToClientPipe; + public async Task ExecuteRequestAsync(string methodName, TRequestType request, CancellationToken cancellationToken) where TRequestType : class { var result = await _clientRpc.InvokeWithParameterObjectAsync(methodName, request, cancellationToken: cancellationToken); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs index e028c0a8dbc3..129149d4b309 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServer.Services; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; @@ -19,7 +20,6 @@ internal sealed class LanguageServerTestComposition var devKitDependencyPath = includeDevKitComponents ? TestPaths.GetDevKitExtensionPath() : null; var serverConfiguration = new ServerConfiguration(LaunchDebugger: false, LogConfiguration: new LogConfiguration(LogLevel.Trace), - StarredCompletionsPath: null, TelemetryLevel: null, SessionId: null, ExtensionAssemblyPaths: extensionPaths ?? [], @@ -28,7 +28,10 @@ internal sealed class LanguageServerTestComposition CSharpDesignTimePath: null, ExtensionLogDirectory: string.Empty, ServerPipeName: null, - UseStdIo: false); + UseStdIo: false, + AutoLoadProjects: false, + SourceGeneratorExecutionPreference: SourceGeneratorExecutionPreference.Balanced, + ClientProcessId: null); var extensionManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory); var assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VSCodeSettingsTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VSCodeSettingsTests.cs new file mode 100644 index 000000000000..b52f5883c950 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VSCodeSettingsTests.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.Extensions.Logging.Abstractions; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public sealed class VSCodeSettingsTests : IDisposable +{ + private readonly TempRoot _tempRoot = new(); + + public void Dispose() + => _tempRoot.Dispose(); + + [Fact] + public void TryRead_ReadsDotnetDefaultSolution() + { + var settings = TryReadSettings(""" + { + "dotnet.defaultSolution": "src/App.sln" + } + """); + + Assert.Equal("src/App.sln", settings.TryGetStringSetting(VSCodeSettings.Names.DefaultSolution)); + } + + [Fact] + public void TryRead_AllowsJsonCommentsAndTrailingCommas() + { + var settings = TryReadSettings(""" + { + // comment + "dotnet.defaultSolution": "src/App.sln", + } + """); + + Assert.Equal("src/App.sln", settings.TryGetStringSetting(VSCodeSettings.Names.DefaultSolution)); + } + + [Fact] + public void TryRead_ReadsDisableDefaultSolutionValue() + { + var settings = TryReadSettings(""" + { + "dotnet.defaultSolution": "disable" + } + """); + + Assert.Equal("disable", settings.TryGetStringSetting(VSCodeSettings.Names.DefaultSolution)); + } + + private string WriteSettingsFile(string settingsJson) + { + var folder = _tempRoot.CreateDirectory(); + var settingsDirectory = folder.CreateDirectory(".vscode"); + var settingsPath = Path.Combine(settingsDirectory.Path, "settings.json"); + File.WriteAllText(settingsPath, settingsJson); + return settingsPath; + } + + private VSCodeSettings TryReadSettings(string settingsJson) + { + var settingsPath = WriteSettingsFile(settingsJson); + Assert.True(VSCodeSettings.TryRead(settingsPath, NullLogger.Instance, out var settings)); + return settings; + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs index b4809eb519cd..522852ba8cd5 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/VirtualProjectXmlProviderTests.cs @@ -94,6 +94,7 @@ await globalJsonFile.WriteAllTextAsync(""" var logger = LoggerFactory.CreateLogger(); var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, logger, CancellationToken.None); + Assert.NotNull(contentNullable); var content = contentNullable.Value; var virtualProjectXml = content.VirtualProjectXml; logger.LogTrace(virtualProjectXml); @@ -126,6 +127,7 @@ await globalJsonFile.WriteAllTextAsync(""" """); var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, LoggerFactory.CreateLogger(), CancellationToken.None); + Assert.NotNull(contentNullable); var content = contentNullable.Value; LoggerFactory.CreateLogger().LogTrace(content.VirtualProjectXml); @@ -177,6 +179,7 @@ await globalJsonFile.WriteAllTextAsync(""" """); var contentNullable = await projectProvider.GetVirtualProjectContentAsync(appFile.Path, LoggerFactory.CreateLogger(), CancellationToken.None); + Assert.NotNull(contentNullable); var content = contentNullable.Value; var diagnostic = content.Diagnostics.Single(); Assert.Contains("Unrecognized directive 'BAD'", diagnostic.Message); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceBridgeProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceBridgeProvider.cs index e622a62c4013..ccda650acd55 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceBridgeProvider.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceBridgeProvider.cs @@ -59,7 +59,7 @@ async Task ProfferServicesToRemoteAsync() .WithTraceSource(_brokeredServiceTraceSource) .ConstructRpc(relayServiceBroker, profferedServiceBrokerChannel); - await relayServiceBroker.Completion; + await relayServiceBroker.Completion.WaitAsync(cancellationToken); } async Task ConsumeServicesFromRemoteAsync() @@ -71,7 +71,7 @@ async Task ConsumeServicesFromRemoteAsync() using (container.ProfferRemoteBroker(remoteClient, bridgeMxStream, ServiceSource.OtherProcessOnSameMachine, [.. Descriptors.RemoteServicesToRegister.Keys])) { - await consumingServiceBrokerChannel.Completion; + await consumingServiceBrokerChannel.Completion.WaitAsync(cancellationToken); } } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerFactory.cs index 1216d9d30390..b2d51e3612d6 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerFactory.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerFactory.cs @@ -87,12 +87,19 @@ public async Task CreateAndConnectAsync(string brokeredServicePipeName) _bridgeCompletionTask = bridgeProvider.SetupBrokeredServicesBridgeAsync(brokeredServicePipeName, _container!, _cancellationTokenSource.Token); } - public Task ShutdownAndWaitForCompletionAsync() + public async Task ShutdownAndWaitForCompletionAsync() { _cancellationTokenSource.Cancel(); - // Return the task we created when we created the bridge; if we never started it in the first place, we'll just return the + // Await the task we created when we created the bridge; if we never started it in the first place, we'll just return the // completed task set in the constructor, so the waiter no-ops. - return _bridgeCompletionTask; + try + { + await _bridgeCompletionTask; + } + catch (OperationCanceledException) + { + // Expected during shutdown, swallow. + } } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Descriptors.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Descriptors.cs index 69f0e25d2bd5..9f191b471ee6 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Descriptors.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Descriptors.cs @@ -43,9 +43,6 @@ internal sealed class Descriptors { RemoteProjectInitializationStatusService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, { BrokeredServiceDescriptors.SolutionSnapshotProvider.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, { BrokeredServiceDescriptors.DebuggerManagedHotReloadService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, - { BrokeredServiceDescriptors.HotReloadSessionNotificationService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, - { BrokeredServiceDescriptors.ManagedHotReloadAgentManagerService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, - { BrokeredServiceDescriptors.GenericHotReloadAgentManagerService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, { BrokeredServiceDescriptors.HotReloadOptionService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, { BrokeredServiceDescriptors.HotReloadLoggerService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, { BrokeredServiceDescriptors.MauiLaunchCustomizerService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs deleted file mode 100644 index b46bdd3a8e20..000000000000 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs +++ /dev/null @@ -1,384 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Features.Workspaces; -using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; -using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.ProjectSystem; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.CodeAnalysis.Shared.Utilities; -using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; -using Microsoft.Extensions.Logging; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; - -/// -/// Handles loading miscellaneous files that are not file-based programs. -/// These files are loaded into a canonical project backed by an empty .cs file in temp. -/// -internal sealed class CanonicalMiscFilesProjectLoader : LanguageServerProjectLoader, IDisposable -{ - private readonly Lazy _canonicalDocumentPath; - - /// - /// Avoid showing restore notifications for misc files - it ends up being noisy and confusing - /// as every file is a misc file on first open until we detect a project for it. - /// - protected override bool EnableProgressReporting => false; - - public CanonicalMiscFilesProjectLoader( - LanguageServerWorkspaceFactory workspaceFactory, - IFileChangeWatcher fileChangeWatcher, - IGlobalOptionService globalOptionService, - ILoggerFactory loggerFactory, - IAsynchronousOperationListenerProvider listenerProvider, - ProjectLoadTelemetryReporter projectLoadTelemetry, - ServerConfigurationFactory serverConfigurationFactory, - IBinLogPathProvider binLogPathProvider, - DotnetCliHelper dotnetCliHelper) - : base( - workspaceFactory, - fileChangeWatcher, - globalOptionService, - loggerFactory, - listenerProvider, - projectLoadTelemetry, - serverConfigurationFactory, - binLogPathProvider, - dotnetCliHelper) - { - _canonicalDocumentPath = new Lazy(() => - { - // Create a temp directory for the canonical project - var tempDirectory = Path.Combine(Path.GetTempPath(), "roslyn-canonical-misc", Guid.NewGuid().ToString()); - Directory.CreateDirectory(tempDirectory); - - var documentPath = Path.Combine(tempDirectory, "Canonical.cs"); - - // Create the empty canonical document - File.WriteAllText(documentPath, string.Empty); - - return documentPath; - }); - } - - /// - /// Adds a miscellaneous document to the canonical project. - /// If the canonical project doesn't exist, creates a primordial project and starts loading the canonical project. - /// - /// - /// The LSP workspace manager and queue ensure that and are not called concurrently. - /// - public async ValueTask AddMiscellaneousDocumentAsync(string documentPath, SourceText documentText, CancellationToken cancellationToken) - { - // Project loading happens asynchronously, so we need to execute this under the load gate to ensure consistency. - return await ExecuteUnderGateAsync(async loadedProjects => - { - var canonicalDocumentPath = _canonicalDocumentPath.Value; - - // Check the current state of the canonical project - if (loadedProjects.TryGetValue(canonicalDocumentPath, out var loadState)) - { - if (loadState is ProjectLoadState.LoadedTargets loadedTargets) - { - // Case 1: Canonical project is fully loaded with targets - // We always expect that the canonical project is either Primordial, or loaded with exactly 1 target (1 TFM). - Contract.ThrowIfFalse(loadedTargets.LoadedProjectTargets.Length == 1, "Expected exactly one loaded target for canonical project"); - return await ForkCanonicalProjectAndAddDocument_NoLockAsync(documentPath, documentText, cancellationToken); - } - else - { - // Case 2: Primordial canonical project was already created, but hasn't finished loading. - var primordialTarget = loadState as ProjectLoadState.Primordial; - Contract.ThrowIfNull(primordialTarget, "Expected primordial target"); - return await AddDocumentToPrimordialProject_NoLockAsync(documentPath, documentText, primordialTarget.PrimordialProjectId, cancellationToken); - } - } - else - { - // Case 3: Canonical project doesn't exist at all - return CreatePrimordialProjectAndAddDocument_NoLock(documentPath, documentText); - } - }, cancellationToken); - } - - /// - /// Removes a miscellaneous document from the canonical project. - /// The canonical project itself is never removed. - /// - /// - /// The LSP workspace manager and queue ensure that and are not called concurrently. - /// - public async ValueTask TryRemoveMiscellaneousDocumentAsync(string documentPath, CancellationToken cancellationToken) - { - // Project loading happens asynchronously, so we need to execute this under the load gate to ensure consistency. - return await ExecuteUnderGateAsync(async loadedProjects => - { - // Try to find and remove the document from the miscellaneous workspace only - var solution = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace.CurrentSolution; - - // Filter to actual documents, ignoring additional documents like Razor files etc. - var documentIds = solution.GetDocumentIdsWithFilePath(documentPath).WhereAsArray(id => solution.GetDocument(id) is not null); - if (documentIds.Length > 0) - { - await _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspaceAsync(workspace => - { - foreach (var documentId in documentIds) - { - workspace.OnDocumentRemoved(documentId); - } - }, cancellationToken); - - return true; - } - - return false; - }, cancellationToken); - } - - private async ValueTask ForkCanonicalProjectAndAddDocument_NoLockAsync(string documentPath, SourceText documentText, CancellationToken cancellationToken) - { - var newProjectId = ProjectId.CreateNewId(debugName: $"Forked Misc Project for '{documentPath}'"); - var newDocumentInfo = DocumentInfo.Create( - DocumentId.CreateNewId(newProjectId), - name: Path.GetFileName(documentPath), - loader: TextLoader.From(TextAndVersion.Create(documentText, VersionStamp.Create())), - filePath: documentPath); - - var forkedProjectInfo = await GetForkedProjectInfoAsync(GetCanonicalProject(), newDocumentInfo, documentText, GlobalOptionService, cancellationToken); - - await _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspaceAsync(workspace => - { - workspace.OnProjectAdded(forkedProjectInfo); - }, cancellationToken); - - var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; - var addedDocument = miscWorkspace.CurrentSolution.GetRequiredDocument(newDocumentInfo.Id); - return addedDocument; - } - - internal async ValueTask IsCanonicalProjectLoadedAsync(CancellationToken cancellationToken) - { - return await ExecuteUnderGateAsync(async loadedProjects => - { - var canonicalDocumentPath = _canonicalDocumentPath.Value; - return loadedProjects.TryGetValue(canonicalDocumentPath, out var loadState) - && loadState is ProjectLoadState.LoadedTargets; - }, cancellationToken); - } - - private async ValueTask AddDocumentToPrimordialProject_NoLockAsync(string documentPath, SourceText documentText, ProjectId existingProjectId, CancellationToken cancellationToken) - { - var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; - var documentInfo = DocumentInfo.Create( - DocumentId.CreateNewId(existingProjectId), - name: Path.GetFileName(documentPath), - loader: TextLoader.From(TextAndVersion.Create(documentText, VersionStamp.Create())), - filePath: documentPath); - - await _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspaceAsync(workspace => - { - workspace.OnDocumentAdded(documentInfo); - }, cancellationToken); - - var addedDocument = miscWorkspace.CurrentSolution.GetRequiredDocument(documentInfo.Id); - return addedDocument; - } - - private TextDocument CreatePrimordialProjectAndAddDocument_NoLock(string documentPath, SourceText documentText) - { - var canonicalDocumentPath = _canonicalDocumentPath.Value; - - // Create primordial project with the canonical document - var canonicalText = SourceText.From(string.Empty); - var canonicalLoader = new SourceTextLoader(canonicalText, canonicalDocumentPath); - - var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( - _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace, - canonicalDocumentPath, - canonicalLoader, - new LanguageInformation(LanguageNames.CSharp, scriptExtension: null), - canonicalText.ChecksumAlgorithm, - _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace.Services.SolutionServices, - metadataReferences: []); - - // Add the project first, then add the requested document - _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspace(workspace => - { - workspace.OnProjectAdded(projectInfo); - }); - - // Now add the requested document - var documentInfo = DocumentInfo.Create( - DocumentId.CreateNewId(projectInfo.Id), - name: Path.GetFileName(documentPath), - loader: new SourceTextLoader(documentText, documentPath), - filePath: documentPath); - - _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspace(workspace => - { - workspace.OnDocumentAdded(documentInfo); - }); - - // Begin loading the canonical project with a design-time build - BeginLoadingProjectWithPrimordial_NoLock( - canonicalDocumentPath, - _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, - projectInfo.Id, - doDesignTimeBuild: true); - - // Return the requested document (not the canonical one) - var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; - var addedDocument = miscWorkspace.CurrentSolution.GetRequiredDocument(documentInfo.Id); - return addedDocument; - } - - protected override async Task TryLoadProjectInMSBuildHostAsync( - BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken) - { - // Set the FileBasedProgram feature flag so that '#:' is permitted without errors in rich misc files. - // This allows us to avoid spurious errors for files which contain '#:' directives yet are not treated as file-based programs (due to not being saved to disk, for example.) - var virtualProjectXml = $""" - - - net$(BundledNETCoreAppTargetFrameworkVersion) - enable - enable - $(Features);FileBasedProgram - - - """; - - // When loading a virtual project, the path to the on-disk source file is not used. Instead the path is adjusted to end with .csproj. - // This is necessary in order to get msbuild to apply the standard c# props/targets to the project. - var virtualProjectPath = VirtualProjectXmlProvider.GetVirtualProjectPath(documentPath); - - const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore; - var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, virtualProjectPath, dotnetPath: null, cancellationToken); - var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectXml, languageName: LanguageNames.CSharp, cancellationToken); - - return new RemoteProjectLoadResult - { - ProjectFile = loadedFile, - ProjectFactory = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, - IsFileBasedProgram = false, - IsMiscellaneousFile = true, - PreferredBuildHostKind = buildHostKind, - ActualBuildHostKind = buildHostKind, - }; - } - - protected override async ValueTask OnProjectUnloadedAsync(string projectFilePath) - { - // Nothing special to do on unload for canonical project - } - - protected override async ValueTask TransitionPrimordialProjectToLoaded_NoLockAsync( - string projectPath, - ProjectSystemProjectFactory primordialProjectFactory, - ProjectId primordialProjectId, - CancellationToken cancellationToken) - { - // We only pass 'doDesignTimeBuild: true' for the canonical project. So that's the only time we should get called back for this. - Contract.ThrowIfFalse(projectPath == _canonicalDocumentPath.Value); - - // Transfer any misc documents from the primordial project to the loaded canonical project - var primordialWorkspace = primordialProjectFactory.Workspace; - var primordialProject = primordialWorkspace.CurrentSolution.GetRequiredProject(primordialProjectId); - - // Get all misc documents (excluding the canonical document) - var miscDocuments = primordialProject.Documents - .Where(d => !PathUtilities.Comparer.Equals(d.FilePath, _canonicalDocumentPath.Value)) - .ToImmutableArray(); - - // Add all misc documents to the loaded project - var loadedProjectId = GetCanonicalProject().Id; - - foreach (var miscDoc in miscDocuments) - { - Contract.ThrowIfNull(miscDoc.FilePath); - await ForkCanonicalProjectAndAddDocument_NoLockAsync(miscDoc.FilePath, await miscDoc.GetTextAsync(cancellationToken), cancellationToken); - } - - // Now remove the primordial project - await primordialProjectFactory.ApplyChangeToWorkspaceAsync( - workspace => workspace.OnProjectRemoved(primordialProjectId), - cancellationToken); - } - - /// - /// Creates a new project based on the canonical project with a new document added. - /// This should only be called when the canonical project is in the FullyLoaded state. - /// - private static async Task GetForkedProjectInfoAsync(Project canonicalProject, DocumentInfo newDocumentInfo, SourceText documentText, IGlobalOptionService globalOptionService, CancellationToken cancellationToken) - { - var newDocumentPath = newDocumentInfo.FilePath; - Contract.ThrowIfNull(newDocumentPath); - - var forkedProjectId = ProjectId.CreateNewId(debugName: $"Forked Misc Project for '{newDocumentPath}'"); - var syntaxTree = CSharpSyntaxTree.ParseText(text: documentText, canonicalProject.ParseOptions as CSharpParseOptions, path: newDocumentPath, cancellationToken); - var hasAllInformation = await VirtualProjectXmlProvider.ShouldReportSemanticErrorsInPossibleFileBasedProgramAsync(globalOptionService, syntaxTree, cancellationToken); - var forkedProjectAttributes = new ProjectInfo.ProjectAttributes( - newDocumentInfo.Id.ProjectId, - version: VersionStamp.Create(), - name: canonicalProject.Name, - assemblyName: canonicalProject.AssemblyName, - language: canonicalProject.Language, - compilationOutputInfo: default, - checksumAlgorithm: SourceHashAlgorithm.Sha1, - filePath: newDocumentPath, - outputFilePath: canonicalProject.OutputFilePath, - outputRefFilePath: canonicalProject.OutputRefFilePath, - hasAllInformation: hasAllInformation); - - var forkedProjectInfo = ProjectInfo.Create( - attributes: forkedProjectAttributes, - compilationOptions: canonicalProject.CompilationOptions, - parseOptions: canonicalProject.ParseOptions, - documents: [newDocumentInfo, .. await Task.WhenAll(canonicalProject.Documents.Select(document => GetDocumentInfoAsync(document, document.FilePath)))], - projectReferences: canonicalProject.ProjectReferences, - metadataReferences: canonicalProject.MetadataReferences, - analyzerReferences: canonicalProject.AnalyzerReferences, - analyzerConfigDocuments: await canonicalProject.AnalyzerConfigDocuments.SelectAsArrayAsync(async document => await GetDocumentInfoAsync(document, document.FilePath)), - additionalDocuments: await canonicalProject.AdditionalDocuments.SelectAsArrayAsync(async document => await GetDocumentInfoAsync(document, document.FilePath))); - return forkedProjectInfo; - - async Task GetDocumentInfoAsync(TextDocument document, string? documentPath) => - DocumentInfo.Create( - DocumentId.CreateNewId(forkedProjectId), - name: Path.GetFileName(documentPath) ?? "", - loader: TextLoader.From(TextAndVersion.Create(await document.GetTextAsync(cancellationToken).ConfigureAwait(false), VersionStamp.Create())), - filePath: documentPath); - } - - private Project GetCanonicalProject() - { - var miscWorkspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; - var project = miscWorkspace.CurrentSolution.Projects - .Single(p => PathUtilities.Comparer.Equals(p.FilePath, _canonicalDocumentPath.Value)); - - return project; - } - - public void Dispose() - { - if (_canonicalDocumentPath.IsValueCreated) - { - var canonicalTempDirectory = Path.GetDirectoryName(_canonicalDocumentPath.Value); - IOUtilities.PerformIO(() => - { - if (Directory.Exists(canonicalTempDirectory)) - { - Directory.Delete(canonicalTempDirectory, recursive: true); - } - }); - } - } -} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscellaneousFilesProjectProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscellaneousFilesProjectProvider.cs new file mode 100644 index 000000000000..6e0104e6e126 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscellaneousFilesProjectProvider.cs @@ -0,0 +1,86 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.Extensions.Logging; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; + +internal sealed class CanonicalMiscellaneousFilesProjectProvider : IDisposable +{ + private readonly LanguageServerWorkspaceFactory _workspaceFactory; + private readonly ILoggerFactory _loggerFactory; + private readonly AsyncLazy> _canonicalBuildResult; + private string? _tempDirectory; + + public CanonicalMiscellaneousFilesProjectProvider(LanguageServerWorkspaceFactory workspaceFactory, ILoggerFactory loggerFactory) + { + _workspaceFactory = workspaceFactory; + _loggerFactory = loggerFactory; + _canonicalBuildResult = AsyncLazy.Create(LoadCanonicalProjectAsync); + } + + public async Task> GetProjectInfoAsync(string miscDocumentPath, CancellationToken cancellationToken) + { + var canonicalInfos = await _canonicalBuildResult.GetValueAsync(cancellationToken); + var miscDocFileInfo = new DocumentFileInfo( + filePath: miscDocumentPath, + logicalPath: Path.GetFileName(miscDocumentPath), + isLinked: false, + isGenerated: false, + folders: []); + + var forkedInfos = canonicalInfos.SelectAsArray(info => info with + { + FilePath = miscDocumentPath, + Documents = info.Documents.Add(miscDocFileInfo), + FileGlobs = [], + }); + + return forkedInfos; + } + + private async Task> LoadCanonicalProjectAsync(CancellationToken cancellationToken) + { + // Set the FileBasedProgram feature flag so that '#:' is permitted without errors in rich misc files. + // This allows us to avoid spurious errors for files which contain '#:' directives yet are not treated + // as file-based programs (due to not being saved to disk, for example.) + var virtualProjectXml = $""" + + + net$(BundledNETCoreAppTargetFrameworkVersion) + enable + enable + $(Features);FileBasedProgram + + + """; + + _tempDirectory = Path.Combine(Path.GetTempPath(), "roslyn-canonical-misc", Guid.NewGuid().ToString()); + + Directory.CreateDirectory(_tempDirectory); + var virtualProjectPath = Path.Combine(_tempDirectory, "Canonical.csproj"); + + await using var buildHostProcessManager = new BuildHostProcessManager( + _workspaceFactory.HostWorkspace.Services.SolutionServices.GetSupportedLanguages(), + globalMSBuildProperties: [], + binaryLogPathProvider: null, + _loggerFactory); + var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, virtualProjectPath, dotnetPath: null, cancellationToken); + var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectXml, languageName: LanguageNames.CSharp, cancellationToken); + return await loadedFile.GetProjectFileInfosAsync(cancellationToken); + } + + public void Dispose() + { + if (_tempDirectory is not null) + { + IOUtilities.PerformIO(() => Directory.Delete(_tempDirectory, recursive: true)); + } + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CsprojInConeChecker.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CsprojInConeChecker.cs new file mode 100644 index 000000000000..ca8a7aad7ae3 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CsprojInConeChecker.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; + +[Shared] +[ExportLspServiceFactory(typeof(CsprojInConeChecker), ProtocolConstants.RoslynLspLanguagesContract)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CsprojInConeCheckerFactory() : ILspServiceFactory +{ + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + return new CsprojInConeChecker(); + } +} + +internal sealed class CsprojInConeChecker : ILspService, IOnInitialized +{ + private ImmutableArray _workspaceFolders; + + public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + { + var initializeManager = context.GetRequiredService(); + _workspaceFolders = initializeManager.GetRequiredWorkspaceFolderPaths(); + return Task.CompletedTask; + } + + public bool IsContainedInCsprojCone(string csFilePath) + { + // Note: manual perf testing of this check on Windows, in a reasonably complex case, + // showed an overhead on the order of 100s of microseconds for this check. + // If this overhead becomes problematic, we may want to put a cache in front of it. + + Contract.ThrowIfTrue(_workspaceFolders.IsDefault, $"{nameof(OnInitializedAsync)} must be called before {nameof(IsContainedInCsprojCone)}."); + if (_workspaceFolders.IsEmpty) + return false; + + if (!PathUtilities.IsAbsolute(csFilePath)) + return false; + + foreach (var workspaceFolder in _workspaceFolders) + { + var directoryName = PathUtilities.GetDirectoryName(csFilePath); + while (PathUtilities.IsSameDirectoryOrChildOf(child: directoryName, parent: workspaceFolder)) + { + var containsCsproj = Directory.EnumerateFiles(directoryName, "*.csproj").Any(); + if (containsCsproj) + return true; + + directoryName = PathUtilities.GetDirectoryName(directoryName); + } + } + + return false; + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs new file mode 100644 index 000000000000..41a1154b1525 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs @@ -0,0 +1,365 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Buffers; +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using System.IO.Enumeration; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Features.Workspaces; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Logging; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; + +[Shared] +[ExportLspServiceFactory(typeof(FileBasedProgramsEntryPointDiscovery), ProtocolConstants.RoslynLspLanguagesContract)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class FileBasedProgramsEntryPointDiscoveryFactory(IGlobalOptionService globalOptionService, IAsynchronousOperationListenerProvider listenerProvider, ILoggerFactory loggerFactory) : ILspServiceFactory +{ + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + return new FileBasedProgramsEntryPointDiscovery(globalOptionService, listenerProvider.GetListener(FeatureAttribute.Workspace), loggerFactory, lspServices); + } +} + +internal sealed partial class FileBasedProgramsEntryPointDiscovery( + IGlobalOptionService globalOptionService, IAsynchronousOperationListener listener, ILoggerFactory loggerFactory, LspServices lspServices) : ILspService, IOnInitialized +{ + private static readonly StringComparer s_pathComparer = StringComparer.OrdinalIgnoreCase; + + /// Directories which are ignored per convention. + /// Some conventional directories like '.git' and '.vs' are expected to be marked hidden and will be automatically ignored by discovery. + private static readonly SearchValues s_ignoredDirectories = SearchValues.Create([ + "artifacts", + "bin", + "obj", + "node_modules" + ], StringComparison.OrdinalIgnoreCase); + + private readonly ILogger _logger = loggerFactory.CreateLogger(); + private ImmutableArray _workspaceFolders; + + public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + { + var initializeManager = context.GetRequiredService(); + _workspaceFolders = initializeManager.GetRequiredWorkspaceFolderPaths(); + Task.Run(async () => + { + try + { + using var token = listener.BeginAsyncOperation(nameof(FindAndLoadEntryPointsAsync)); + await FindAndLoadEntryPointsAsync(); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex)) + { + throw ExceptionUtilities.Unreachable(); + } + }, cancellationToken); + + return Task.CompletedTask; + } + + internal async Task FindAndLoadEntryPointsAsync() + { + Contract.ThrowIfTrue(_workspaceFolders.IsDefault, $"{nameof(OnInitializedAsync)} must be called before {nameof(FindAndLoadEntryPointsAsync)}."); + + if (_workspaceFolders.IsEmpty) + { + _logger.LogTrace("No workspace folders to search for file-based apps."); + return; + } + + if (!globalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms)) + { + _logger.LogTrace(@"""dotnet.projects.enableFileBasedPrograms"" is false. Not discovering entry points."); + return; + } + + if (!globalOptionService.GetOption(FileBasedAppsOptionsStorage.EnableAutomaticDiscovery)) + { + _logger.LogTrace(@"""dotnet.fileBasedApps.enableAutomaticDiscovery"" is false. Not discovering entry points."); + return; + } + + var fileBasedProgramsProjectSystem = (FileBasedProgramsProjectSystem?)lspServices.GetService(); + Contract.ThrowIfNull(fileBasedProgramsProjectSystem); + + // Note: the overwhelmingly common case is when there is just one workspace folder. + // For simplicity we orient our search around one workspace folder at a time. + foreach (var workspaceFolder in _workspaceFolders) + { + foreach (var fileBasedAppPath in FindEntryPoints(workspaceFolder)) + { + await fileBasedProgramsProjectSystem.TryBeginLoadingFileBasedAppAsync(fileBasedAppPath); + } + } + + // Discovery pass done. Find and delete old caches. + IOUtilities.PerformIO(() => + { + using var enumerator = new OldCacheEnumerator(); + while (enumerator.MoveNext()) + { + IOUtilities.PerformIO(() => Directory.Delete(enumerator.Current, recursive: true)); + } + }); + } + + private sealed class OldCacheEnumerator() : FileSystemEnumerator( + directory: VirtualProjectXmlProvider.GetDiscoveryCacheRootDirectory(), + options: new() { RecurseSubdirectories = false }) + { + // Yield cache directories that have not been modified in 30 days (indicates they are stale and should be deleted) + private readonly DateTimeOffset _includeCachesEarlierThanUtc = DateTimeOffset.UtcNow - TimeSpan.FromDays(30); + + protected override string TransformEntry(ref FileSystemEntry entry) => entry.ToFullPath(); + + protected override bool ShouldIncludeEntry(ref FileSystemEntry entry) + { + return entry.IsDirectory && entry.LastWriteTimeUtc < _includeCachesEarlierThanUtc; + } + } + + internal ImmutableArray FindEntryPoints(string workspaceFolder) + { + var stopwatch = SharedStopwatch.StartNew(); + var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(workspaceFolder); + var cacheFilePath = Path.Join(cacheDirectory, "cache.json"); + Cache? cache = null; + try + { + if (File.Exists(cacheFilePath)) + { + using var cacheFile = File.OpenRead(cacheFilePath); + cache = JsonSerializer.Deserialize(cacheFile, CacheSerializerContext.Default.Cache); + } + + // Drop malformed caches + if (cache != null + && (!cache.WorkspacePath.Equals(workspaceFolder, StringComparison.OrdinalIgnoreCase) + || cache.FileBasedAppFullPaths.IsDefault + || cache.DirectoriesContainingCsproj.IsDefault)) + { + cache = null; + } + } + catch (Exception ex) + { + _logger.LogDebug("Could not read cache file: {ex.Message}", ex.Message); + } + + cache ??= new Cache(workspaceFolder, DateTimeOffset.MinValue, FileBasedAppFullPaths: [], DirectoriesContainingCsproj: []); + + // Note: file system timestamps can have a coarser resolution than DateTimeOffset. + // This means that using APIs like `DateTimeOffset.UtcNow`, then writing a file, then sampling its LastWriteTimeUtc, + // can result in the UtcNow that we accessed earlier, having a "later" value than the LastWriteTimeUtc. + // + // To deal with this accurately, we write a timestamp file to the filesystem, then get a walkStartTimeUtc from its LastWriteTimeUtc. + // We assume that file writes in the workspace folder which occur after this write, will have equal or later timestamps. + // Timestamps we encounter which compare equal to the walkStartTimeUtc timestamp, must be treated as possibly being newer than the walkStartTimeUtc timestamp. + var walkStartTimeUtc = IOUtilities.PerformIO(() => + { + var sentinelPath = Path.Join(cacheDirectory, $".walk-timestamp-{Guid.NewGuid()}"); + File.WriteAllBytes(sentinelPath, []); + var lastWriteTime = File.GetLastWriteTimeUtc(sentinelPath); + File.Delete(sentinelPath); + return lastWriteTime; + }, defaultValue: cache.LastWalkTimeUtc); + + var newFileBasedAppsBuilder = ArrayBuilder.GetInstance(cache.FileBasedAppFullPaths.Length); + var directoriesContainingCsprojBuilder = ArrayBuilder.GetInstance(cache.DirectoriesContainingCsproj.Length); + var visitor = new WorkspaceFolderVisitor(cache, newFileBasedAppsBuilder, directoriesContainingCsprojBuilder, _logger); + visitor.Visit(); + var elapsedMilliseconds = Math.Round(stopwatch.Elapsed.TotalMilliseconds); + _logger.LogInformation("Finished discovery in '{workspaceFolder}' in {elapsedMilliseconds} milliseconds", workspaceFolder, elapsedMilliseconds); + + // Ensure items go into the cache file in a stable order. + // This is useful for manual inspection and allows use of 'BinarySearch' to match directories against the cache. + newFileBasedAppsBuilder.Sort(); + directoriesContainingCsprojBuilder.Sort(); + var newCache = new Cache(workspaceFolder, walkStartTimeUtc, newFileBasedAppsBuilder.ToImmutableAndFree(), directoriesContainingCsprojBuilder.ToImmutableAndFree()); + try + { + Directory.CreateDirectory(cacheDirectory); + var cacheStagingFilePath = Path.Join(cacheDirectory, "cache.staging.json"); + using (var stagingFile = File.Create(cacheStagingFilePath)) + { + JsonSerializer.Serialize(stagingFile, newCache, CacheSerializerContext.Default.Cache); + } + File.Replace(cacheStagingFilePath, cacheFilePath, destinationBackupFileName: null); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex)) + { + } + + return newCache.FileBasedAppFullPaths; + } + + /// Check if discovery should consider this a file-based app. + private static bool IsFileBasedApp(string fullPath) + { + using var fileStream = File.OpenRead(fullPath); + var toRead = (int)Math.Min(5, fileStream.Length); + InlineArray5 bytes = default; + Span bytesSpan = bytes; + fileStream.ReadExactly(bytesSpan[..toRead]); + + // Discovery only considers a file to be file-based app, if it starts with either "#!", or UTF-8 BOM followed by "#!". + return bytesSpan is [(byte)'#', (byte)'!', ..] or [0xEF, 0xBB, 0xBF, (byte)'#', (byte)'!']; + } + + private enum CsFileKind + { + None, // Denotes a file that is irrelevant for discovery. Shouldn't appear on a valid 'CsFileInfo' instance. + Directory, + Cs, + Csproj, + } + + private readonly struct CsFileInfo(CsFileKind kind, string path, DateTimeOffset createdOrModifiedTimeUtc) + { + public CsFileKind Kind { get; } = kind; + public string Path { get; } = path; + public DateTimeOffset CreatedOrModifiedTimeUtc { get; } = createdOrModifiedTimeUtc; + } + + private class DirectoryEnumerator(string directory) : FileSystemEnumerator(directory) + { + private CsFileKind GetKind(ref FileSystemEntry entry) + { + if (entry.IsDirectory) + return CsFileKind.Directory; + + var extension = Path.GetExtension(entry.FileName); + if (extension.Equals(".cs", StringComparison.OrdinalIgnoreCase)) + return CsFileKind.Cs; + + if (extension.Equals(".csproj", StringComparison.OrdinalIgnoreCase)) + return CsFileKind.Csproj; + + return CsFileKind.None; + } + + protected override CsFileInfo TransformEntry(ref FileSystemEntry entry) + { + var kind = GetKind(ref entry); + Contract.ThrowIfTrue(kind == CsFileKind.None); + return new CsFileInfo(kind, entry.ToFullPath(), Max(entry.CreationTimeUtc, entry.LastWriteTimeUtc)); + } + + protected override bool ShouldIncludeEntry(ref FileSystemEntry entry) + { + return GetKind(ref entry) != CsFileKind.None; + } + + protected override bool ShouldRecurseIntoEntry(ref FileSystemEntry entry) + { + throw ExceptionUtilities.Unreachable(); + } + } + + private class WorkspaceFolderVisitor(Cache cache, ArrayBuilder entryPointsBuilder, ArrayBuilder directoriesContainingCsprojBuilder, ILogger logger) + { + internal void Visit() + // Note: passing `DateTimeOffset.MinValue` here will force `VisitDirectory` to stat the directory again to get its created/modified times out. + => VisitDirectory(cache.WorkspacePath, DateTimeOffset.MinValue); + + private void VisitDirectory(string directory, DateTimeOffset createdOrModifiedTimeUtc) + { + if (Path.GetFileName(directory.AsSpan()).ContainsAny(s_ignoredDirectories)) + return; + + if (createdOrModifiedTimeUtc < cache.LastWalkTimeUtc) + { + // On NTFS, the directory timestamps we observe when enumerating can be stale when files are added/deleted from a directory. + // If we find the timestamps were old enough (i.e. we entered this block), + // we still need to `new DirectoryInfo()` again and force the timestamps to update if needed. + var directoryInfo = new DirectoryInfo(directory); + var newCreatedOrModifiedTimeUtc = Max(directoryInfo.CreationTimeUtc, directoryInfo.LastWriteTimeUtc); + if (newCreatedOrModifiedTimeUtc < cache.LastWalkTimeUtc && cache.DirectoriesContainingCsproj.BinarySearch(directory, s_pathComparer) >= 0) + { + // Our info about this directory is up to date, and we know it contains a csproj, so bail out before enumerating its files. + directoriesContainingCsprojBuilder.Add(directory); + return; + } + + createdOrModifiedTimeUtc = Max(createdOrModifiedTimeUtc, newCreatedOrModifiedTimeUtc); + } + + using var currentDirectoryItems = TemporaryArray.Empty; + using var enumerator = new DirectoryEnumerator(directory); + while (enumerator.MoveNext()) + { + var fileInfo = enumerator.Current; + if (fileInfo.Kind == CsFileKind.Csproj) + { + // Found a csproj. Return without visiting any of the files. + directoriesContainingCsprojBuilder.Add(directory); + return; + } + + currentDirectoryItems.Add(fileInfo); + } + + // Did not find a csproj. Continue searching this subtree for entry points. + foreach (var fileInfo in currentDirectoryItems) + { + // When a subdirectory is moved in to a parent directory between two discovery passes, the timestamps of the subdirectory's files are not updated. + // Only the "modified" timestamp of the parent directory, and the "created" timestamp of the subdirectory, are updated. + // This means: even if a .cs file we encounter within a "new" subdirectory has old timestamps, we don't know whether we've seen it before or not, so we need to crack it. + if (fileInfo.Kind == CsFileKind.Directory) + VisitDirectory(fileInfo.Path, Max(createdOrModifiedTimeUtc, fileInfo.CreatedOrModifiedTimeUtc)); + else if (fileInfo.Kind == CsFileKind.Cs) + VisitCsFile(fileInfo.Path, Max(createdOrModifiedTimeUtc, fileInfo.CreatedOrModifiedTimeUtc)); + else + throw ExceptionUtilities.Unreachable(); + } + } + + private void VisitCsFile(string file, DateTimeOffset createdOrModifiedTimeUtc) + { + if (createdOrModifiedTimeUtc < cache.LastWalkTimeUtc) + { + if (cache.FileBasedAppFullPaths.BinarySearch(file) >= 0) + { + logger.LogInformation("Discovered file-based app (cache hit): {csFilePath}", file); + entryPointsBuilder.Add(file); + } + + return; + } + + if (IOUtilities.PerformIO(() => IsFileBasedApp(file))) + { + logger.LogInformation("Discovered file-based app (cache miss): {csFilePath}", file); + entryPointsBuilder.Add(file); + } + } + } + + /// Get the later of two DateTimeOffsets. + private static DateTimeOffset Max(DateTimeOffset lhs, DateTimeOffset rhs) + => lhs < rhs ? rhs : lhs; + + internal sealed record Cache(string WorkspacePath, DateTimeOffset LastWalkTimeUtc, ImmutableArray FileBasedAppFullPaths, ImmutableArray DirectoriesContainingCsproj); + + [JsonSerializable(typeof(Cache))] + internal sealed partial class CacheSerializerContext : JsonSerializerContext; +} \ No newline at end of file diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs index 3a50edcfc09b..2f656bdc7dab 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Features.Workspaces; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; @@ -9,11 +12,13 @@ using Microsoft.CodeAnalysis.ProjectSystem; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.Extensions.Logging; using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; @@ -23,12 +28,7 @@ internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoad private readonly ILspServices _lspServices; private readonly ILogger _logger; private readonly VirtualProjectXmlProvider _projectXmlProvider; - private readonly CanonicalMiscFilesProjectLoader _canonicalMiscFilesLoader; - - public void Dispose() - { - _canonicalMiscFilesLoader.Dispose(); - } + private readonly CanonicalMiscellaneousFilesProjectProvider _canonicalProjectProvider; public FileBasedProgramsProjectSystem( ILspServices lspServices, @@ -56,134 +56,256 @@ public FileBasedProgramsProjectSystem( _lspServices = lspServices; _logger = loggerFactory.CreateLogger(); _projectXmlProvider = projectXmlProvider; - _canonicalMiscFilesLoader = new CanonicalMiscFilesProjectLoader( - workspaceFactory, - fileChangeWatcher, - globalOptionService, - loggerFactory, - listenerProvider, - projectLoadTelemetry, - serverConfigurationFactory, - binLogPathProvider, - dotnetCliHelper); + _canonicalProjectProvider = new CanonicalMiscellaneousFilesProjectProvider(workspaceFactory, loggerFactory); + + globalOptionService.AddOptionChangedHandler(this, OnGlobalOptionChanged); } - private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString; + public void Dispose() + { + GlobalOptionService.RemoveOptionChangedHandler(this, OnGlobalOptionChanged); + } - public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument textDocument, CancellationToken cancellationToken) + private void OnGlobalOptionChanged(object sender, object target, OptionChangedEventArgs args) { - // There are a few cases here: - // 1. The document is a primordial document (either not loaded yet or doesn't support design time build) - it will be in the misc files workspace. - // 2. The document is loaded as a canonical misc file - these are always in the misc files workspace. - // 3. The document is loaded as a file based program - then it will be in the main workspace where the project path matches the source file path. - - // NB: The FileBasedProgramsProjectSystem uses the document file path (the on-disk path) as the projectPath in 'IsProjectLoadedAsync'. - var isLoadedAsFileBasedProgram = textDocument.FilePath is { } filePath && await IsProjectLoadedAsync(filePath, cancellationToken); - - // If this document has a file-based program syntactic marker, but we aren't loading it in a file-based programs project, - // we need the caller to remove and re-add this document, so that it gets put in a file-based programs project instead. - // See the check in 'LspWorkspaceManager.GetLspDocumentInfoAsync', which removes a document based on 'IsMiscellaneousFilesDocumentAsync' result, - // then calls 'GetLspDocumentInfoAsync' again for the same request. - if (!isLoadedAsFileBasedProgram && VirtualProjectXmlProvider.IsFileBasedProgram(await textDocument.GetTextAsync(cancellationToken))) - return false; - - if (textDocument.Project.Solution.Workspace == _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace) - { - // Do a check to determine if the misc project needs to be re-created with a new HasAllInformation flag value. - if (!isLoadedAsFileBasedProgram - && await _canonicalMiscFilesLoader.IsCanonicalProjectLoadedAsync(cancellationToken) - && textDocument is Document document - && await document.GetSyntaxTreeAsync(cancellationToken) is { } syntaxTree) + foreach (var (key, value) in args.ChangedOptions) + { + if (key.Option.Equals(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms)) { - var newHasAllInformation = await VirtualProjectXmlProvider.ShouldReportSemanticErrorsInPossibleFileBasedProgramAsync(GlobalOptionService, syntaxTree, cancellationToken); - if (newHasAllInformation != document.Project.State.HasAllInformation) - { - // TODO: replace this method and the call site in LspWorkspaceManager, - // with a mechanism for "updating workspace state if needed" based on changes to a document. - // Perhaps this could be based on actually listening for changes to particular documents, rather than whenever an LSP request related to a document comes in. - // We should be able to do more incremental updates in more cases, rather than needing to throw things away and start over. - return false; - } + // This event handler can't be async, so we ignore the resulting task here, + // and take care that the ignored call doesn't throw an exception + _ = HandleEnableFileBasedProgramsChangedAsync((bool)value!); + break; } + } + async Task HandleEnableFileBasedProgramsChangedAsync(bool value) + { + using var token = Listener.BeginAsyncOperation(nameof(HandleEnableFileBasedProgramsChangedAsync)); + try + { + _logger.LogDebug($"Detected enableFileBasedPrograms changed to '{value}'. Unloading loose file projects."); + await UnloadAllProjectsAsync(); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.General)) + { + throw ExceptionUtilities.Unreachable(); + } + } + } + + private static string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString; + + private bool ClassifyAsMiscellaneousFileWithNoReferences(string filePath, LanguageInformation languageInformation) + { + // 2. Is `enableFileBasedPrograms` enabled? + // - No → Classify as Miscellaneous File With No References + // - Yes → Continue to next check + var enableFileBasedPrograms = GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms); + if (!enableFileBasedPrograms) + { return true; } - if (isLoadedAsFileBasedProgram) + // 3. Is the file a regular C# file? (i.e. not a `.csx` script, and not a file using a language besides C#) + // - No → Classify as Miscellaneous File With No References + // - Yes → Continue to next check + if (languageInformation.LanguageName != LanguageNames.CSharp + || MiscellaneousFileUtilities.IsScriptFile(languageInformation, filePath)) + { return true; + } - // Document is not managed by this project system. Caller should unload it. return false; } - public async ValueTask AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) + private async ValueTask ClassifyDocumentAsync(string filePath, string languageId, CancellationToken cancellationToken) { - var documentFilePath = GetDocumentFilePath(uri); var languageInfoProvider = _lspServices.GetRequiredService(); - if (!languageInfoProvider.TryGetLanguageInformation(uri, languageId, out var languageInformation)) + if (!languageInfoProvider.TryGetLanguageInformation(ProtocolConversions.CreateAbsoluteDocumentUri(filePath), languageId, out var languageInformation)) { - Contract.Fail($"Could not find language information for {uri} with absolute path {documentFilePath}"); + Contract.Fail($"Could not find language information for '{filePath}'"); } - var supportsDesignTimeBuild = languageInformation.LanguageName == LanguageNames.CSharp - && GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms); + // The design of this is described in docs/features/file-based-programs-vscode.md + // Note: Step (1) is skipped, as we assume a first-chance lookup in the host workspace will handle this case. - // Check if this is a C# file that should use the canonical misc files loader - if (supportsDesignTimeBuild) + // Steps (2) and (3) + if (ClassifyAsMiscellaneousFileWithNoReferences(filePath, languageInformation)) { - // For virtual (non-file) URIs or non-file-based programs, use the canonical loader - if (uri.ParsedUri is null || !uri.ParsedUri.IsFile || !VirtualProjectXmlProvider.IsFileBasedProgram(documentText)) - { - return await _canonicalMiscFilesLoader.AddMiscellaneousDocumentAsync(documentFilePath, documentText, CancellationToken.None); - } + return LooseDocumentKind.MiscellaneousFileWithNoReferences; + } + + // 4. Does the file have an absolute path and exist on disk? (i.e. it is not a "virtual document" created for a new, not-yet-saved file, or similar.) + // - Yes → Go to (5) + // - No → Classify as Miscellaneous File With Standard References + if (!PathUtilities.IsAbsolute(filePath)) + return LooseDocumentKind.MiscellaneousFileWithStandardReferences; + + SourceText? sourceText = IOUtilities.PerformIO(() => + { + // Note: SourceText.From eagerly reads the entire file + using var fileStream = File.OpenRead(filePath); + return SourceText.From(fileStream); + }); + + // File had an absolute path but we were unable to read it, due to it not existing or to some other I/O issue. + if (sourceText is null) + { + return LooseDocumentKind.MiscellaneousFileWithStandardReferences; + } + + // 5. Does the file have `#:` or `#!` directives? + // - Yes → Classify as File-Based App. Restore if needed and show semantic errors. + // - No → Continue to next check + if (VirtualProjectXmlProvider.HasFileBasedAppDirectives(sourceText)) + { + return LooseDocumentKind.FileBasedApp; + } + + // 6. Is `enableFileBasedProgramsWhenAmbiguous` enabled? (default: `false` in release, `true` in prerelease) + // - No → Classify as Miscellaneous File With Standard References + // - Yes → Continue to heuristic detection + + if (!GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableSemanticErrorsInMiscellaneousFiles)) + { + return LooseDocumentKind.MiscellaneousFileWithStandardReferences; } - // Use the original file-based programs logic - var primordialDoc = AddPrimordialDocument(uri, documentText, languageId); - Contract.ThrowIfNull(primordialDoc.FilePath); + // Heuristic Detection: + + // 7. Are top-level statements present? + // - No → Classify as Miscellaneous File With Standard References + // - Yes → Continue to next check + + var syntaxTree = CSharpSyntaxTree.ParseText(sourceText, cancellationToken: cancellationToken); + var containsTopLevelStatements = syntaxTree.GetRoot(cancellationToken) is CompilationUnitSyntax compilationUnit && compilationUnit.Members.Any(SyntaxKind.GlobalStatement); + if (!containsTopLevelStatements) + { + return LooseDocumentKind.MiscellaneousFileWithStandardReferences; + } - var doDesignTimeBuild = uri.ParsedUri?.IsFile is true && supportsDesignTimeBuild; - await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild); + // 8. Is the file included in a `.csproj` cone? + // - Yes → Classify as Miscellaneous File With Standard References (wait for project to load) + // - No → Classify as Miscellaneous File With Standard References and Semantic Errors + var csprojInConeChecker = _lspServices.GetRequiredService(); + if (csprojInConeChecker.IsContainedInCsprojCone(filePath)) + { + return LooseDocumentKind.MiscellaneousFileWithStandardReferences; + } - return primordialDoc; + return LooseDocumentKind.MiscellaneousFileWithStandardReferencesAndSemanticErrors; + } - TextDocument AddPrimordialDocument(DocumentUri uri, SourceText documentText, string languageId) + public async ValueTask AddDocumentAsync(DocumentUri documentUri, TrackedDocumentInfo documentInfo) + { + var languageInfoProvider = _lspServices.GetRequiredService(); + if (!languageInfoProvider.TryGetLanguageInformation(documentUri, documentInfo.LanguageId, out var languageInformation)) { - var workspace = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.Workspace; - var sourceTextLoader = new SourceTextLoader(documentText, documentFilePath); - var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( - workspace, documentFilePath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, workspace.Services.SolutionServices, []); + Contract.Fail($"Could not find language information for '{documentUri}'"); + } - _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(projectInfo)); + var documentFilePath = GetDocumentFilePath(documentUri); + var sourceTextLoader = new SourceTextLoader(documentInfo.SourceText, documentFilePath); + var doDesignTimeBuild = !ClassifyAsMiscellaneousFileWithNoReferences(documentFilePath, languageInformation); + return await this.GetOrLoadEntryPointDocumentAsync( + documentFilePath, sourceTextLoader, languageInformation, documentInfo.SourceText.ChecksumAlgorithm, doDesignTimeBuild); + } - // https://github.com/dotnet/roslyn/pull/78267 - // Work around an issue where opening a Razor file in the misc workspace causes a crash. - if (languageInformation.LanguageName == LanguageInfoProvider.RazorLanguageName) + /// + /// Used to begin loading a file-based app project for a file-based app on disk, if it hasn't started already, + /// when the caller doesn't need to use any results of the loading process. + /// + public async ValueTask TryBeginLoadingFileBasedAppAsync(string documentFilePath) + { + Contract.ThrowIfFalse(PathUtilities.IsAbsolute(documentFilePath)); + var sourceTextLoader = new WorkspaceFileTextLoader(_workspaceFactory.HostWorkspace.CurrentSolution.Services, documentFilePath, defaultEncoding: null); + var languageInfoProvider = _lspServices.GetRequiredService(); + if (!languageInfoProvider.TryGetLanguageInformation(ProtocolConversions.CreateAbsoluteDocumentUri(documentFilePath), lspLanguageId: "csharp", out var languageInformation)) + { + Contract.Fail($"Could not find language information for '{documentFilePath}'"); + } + + await GetOrLoadEntryPointDocumentAsync(documentFilePath, sourceTextLoader, languageInformation, SourceHashAlgorithms.Default, doDesignTimeBuild: true); + } + + public async ValueTask GetOrLoadEntryPointDocumentAsync(string documentFilePath, TextLoader textLoader, LanguageInformation languageInformation, SourceHashAlgorithm checksumAlgorithm, bool doDesignTimeBuild) + { + var project = await base.GetOrLoadProjectAsync(documentFilePath, _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, CreatePrimordialProjectInfo, doDesignTimeBuild); + return project is null ? null : LookupExistingDocument(project); + + TextDocument? LookupExistingDocument(Project project) + { + var document = project.Documents.FirstOrDefault(document => document.FilePath == documentFilePath) + ?? project.AdditionalDocuments.FirstOrDefault(document => document.FilePath == documentFilePath); + if (document is null) { - var docId = projectInfo.AdditionalDocuments.Single().Id; - return workspace.CurrentSolution.GetRequiredAdditionalDocument(docId); + _logger.LogWarning("Could not get a document for '{documentFilePath}' because its project doesn't contain a document for it", documentFilePath); } - var id = projectInfo.Documents.Single().Id; - return workspace.CurrentSolution.GetRequiredDocument(id); + return document; + } + + ProjectInfo CreatePrimordialProjectInfo(ProjectSystemProjectFactory projectFactory) + { + var enableFileBasedPrograms = GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms); + return MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( + projectFactory.Workspace, documentFilePath, textLoader, languageInformation, checksumAlgorithm, projectFactory.Workspace.Services.SolutionServices, [], enableFileBasedPrograms); } } public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri) { + // Note: we intentionally do not unload file-based apps in this path. + // This is because we want to unload from the miscellaneous files workspace only, when a file is found in the host workspace. var documentPath = GetDocumentFilePath(uri); - // First try to remove from the canonical misc files loader if it was created - var removedFromCanonical = await _canonicalMiscFilesLoader.TryRemoveMiscellaneousDocumentAsync(documentPath, CancellationToken.None); - if (removedFromCanonical) - return true; + return await TryUnloadProjectAsync(documentPath, fromProjectFactory: _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory); + } + + public async ValueTask CloseDocumentAsync(DocumentUri uri) + { + // If automatic discovery is enabled, we don't want to unload a file-based app upon closing a document. + var unloadFromProjectFactory = GlobalOptionService.GetOption(FileBasedAppsOptionsStorage.EnableAutomaticDiscovery) + ? _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory + : null; - // Fall back to the file-based programs logic - return await TryUnloadProjectAsync(documentPath); + var documentPath = GetDocumentFilePath(uri); + await TryUnloadProjectAsync(documentPath, unloadFromProjectFactory); } protected override async Task TryLoadProjectInMSBuildHostAsync( BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken) { + // Note: we assume that if we made it this far, the document is for the C# language. + var documentKind = await ClassifyDocumentAsync(documentPath, languageId: "csharp", cancellationToken); + _logger.LogDebug("Classified '{documentPath}' as '{documentKind}'.", documentPath, documentKind); + + if (documentKind == LooseDocumentKind.MiscellaneousFileWithNoReferences) + { + // This might happen due to a race involving changes to option values. + // Just don't proceed with the reload and assume the option change handler will unload this project if needed. + _logger.LogWarning("A document classified as {documentKind} should not be design-time built.", documentKind); + return null; + } + + if (documentKind is LooseDocumentKind.MiscellaneousFileWithStandardReferences or LooseDocumentKind.MiscellaneousFileWithStandardReferencesAndSemanticErrors) + { + return new RemoteProjectLoadResult + { + ProjectFileInfos = await _canonicalProjectProvider.GetProjectInfoAsync(documentPath, cancellationToken).ConfigureAwait(false), + DiagnosticLogItems = [], + ProjectFactory = _workspaceFactory.MiscellaneousFilesWorkspaceProjectFactory, + IsFileBasedProgram = false, + IsMiscellaneousFile = true, + HasAllInformation = documentKind is LooseDocumentKind.MiscellaneousFileWithStandardReferencesAndSemanticErrors, + PreferredBuildHostKind = BuildHostProcessKind.NetCore, + ActualBuildHostKind = BuildHostProcessKind.NetCore, + }; + } + + // Fall through to ordinary file-based app handling. + Contract.ThrowIfFalse(documentKind is LooseDocumentKind.FileBasedApp); + var content = await _projectXmlProvider.GetVirtualProjectContentAsync(documentPath, _logger, cancellationToken); if (content is not var (virtualProjectContent, diagnostics)) { @@ -207,29 +329,14 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri return new RemoteProjectLoadResult { - ProjectFile = loadedFile, - // If we have made it this far, we must have determined that the document is a file-based program. - // TODO: we should assert this somehow. However, we cannot use the on-disk state of the file to do so, because the decision to load this as a file-based program was based on in-editor content. + ProjectFileInfos = await loadedFile.GetProjectFileInfosAsync(cancellationToken), + DiagnosticLogItems = await loadedFile.GetDiagnosticLogItemsAsync(cancellationToken), ProjectFactory = _workspaceFactory.HostProjectFactory, IsFileBasedProgram = true, IsMiscellaneousFile = false, + HasAllInformation = true, PreferredBuildHostKind = buildHostKind, ActualBuildHostKind = buildHostKind, }; } - - protected override async ValueTask OnProjectUnloadedAsync(string projectFilePath) - { - } - - protected override async ValueTask TransitionPrimordialProjectToLoaded_NoLockAsync( - string projectPath, - ProjectSystemProjectFactory primordialProjectFactory, - ProjectId primordialProjectId, - CancellationToken cancellationToken) - { - await primordialProjectFactory.ApplyChangeToWorkspaceAsync( - workspace => workspace.OnProjectRemoved(primordialProjectId), - cancellationToken); - } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/LooseDocumentKind.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/LooseDocumentKind.cs new file mode 100644 index 000000000000..2f4c5c5ac1b2 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/LooseDocumentKind.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; + +/// +/// See /docs/features/file-based-programs-vscode.md#file-based-app-detection +/// +internal enum LooseDocumentKind +{ + MiscellaneousFileWithNoReferences, + MiscellaneousFileWithStandardReferences, + MiscellaneousFileWithStandardReferencesAndSemanticErrors, + FileBasedApp, +} \ No newline at end of file diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs index 5c941111d763..06d740c78f8a 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs @@ -99,15 +99,7 @@ internal class VirtualProjectXmlProvider(DotnetCliHelper dotnetCliHelper) internal static string? GetVirtualProjectPath(string? documentFilePath) => Path.ChangeExtension(documentFilePath, ".csproj"); - /// - /// Indicates whether the editor considers the text to be a file-based program. - /// If this returns false, the text is either a miscellaneous file or is part of an ordinary project. - /// - /// - /// The editor considers the text to be a file-based program if it has any '#!' or '#:' directives at the top. - /// Note that a file with top-level statements but no directives can still work with 'dotnet app.cs' etc. on the CLI, but will be treated as a misc file in the editor. - /// - internal static bool IsFileBasedProgram(SourceText text) + internal static bool HasFileBasedAppDirectives(SourceText text) { var tokenizer = SyntaxFactory.CreateTokenParser(text, CSharpParseOptions.Default.WithFeatures([new("FileBasedProgram", "true")])); var result = tokenizer.ParseLeadingTrivia(); @@ -121,21 +113,6 @@ internal static bool IsFileBasedProgram(SourceText text) return false; } - internal static async Task ShouldReportSemanticErrorsInPossibleFileBasedProgramAsync(IGlobalOptionService globalOptionService, SyntaxTree tree, CancellationToken cancellationToken) - { - if (!globalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms) - || !globalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedProgramsWhenAmbiguous)) - { - return false; - } - - var root = await tree.GetRootAsync(cancellationToken); - if (root is CompilationUnitSyntax compilationUnit) - return compilationUnit.Members.Any(member => member.IsKind(SyntaxKind.GlobalStatement)); - - return false; - } - #region Temporary copy of subset of dotnet run-api behavior for fallback: https://github.com/dotnet/roslyn/issues/78618 // See https://github.com/dotnet/sdk/blob/b5dbc69cc28676ac6ea615654c8016a11b75e747/src/Cli/Microsoft.DotNet.Cli.Utils/Sha256Hasher.cs#L10 private static class Sha256Hasher @@ -157,21 +134,35 @@ public static string HashWithNormalizedCasing(string text) } } + internal static string GetDiscoveryCacheDirectory(string workspaceFolder) + => GetTempPathCore("runfile-discovery", workspaceFolder); + + internal static string GetDiscoveryCacheRootDirectory() + => GetTempDotnetSubdirectory("runfile-discovery"); + // See https://github.com/dotnet/sdk/blob/5a4292947487a9d34f4256c1d17fb3dc26859174/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs#L449 internal static string GetArtifactsPath(string entryPointFileFullPath) + => GetTempPathCore("runfile", entryPointFileFullPath); + + private static string GetTempDotnetSubdirectory(string dotnetSubdirectory) { // We want a location where permissions are expected to be restricted to the current user. - string directory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + string tempDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.GetTempPath() : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + return Path.Join(tempDirectory, "dotnet", dotnetSubdirectory); + } - // Include entry point file name so the directory name is not completely opaque. - string fileName = Path.GetFileNameWithoutExtension(entryPointFileFullPath); - string hash = Sha256Hasher.HashWithNormalizedCasing(entryPointFileFullPath); + private static string GetTempPathCore(string dotnetSubdirectory, string originalFilePath) + { + // Include original file name so the directory name is not completely opaque. + string fileName = Path.GetFileNameWithoutExtension(originalFilePath); + string hash = Sha256Hasher.HashWithNormalizedCasing(originalFilePath); string directoryName = $"{fileName}-{hash}"; - return Path.Join(directory, "dotnet", "runfile", directoryName); + return Path.Join(GetTempDotnetSubdirectory(dotnetSubdirectory), directoryName); } + #endregion // https://github.com/dotnet/roslyn/issues/78618: falling back to this until dotnet run-api is more widely available diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/AutoLoadProjectsInitializer.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/AutoLoadProjectsInitializer.cs new file mode 100644 index 000000000000..db534b04a7e9 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/AutoLoadProjectsInitializer.cs @@ -0,0 +1,175 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Composition; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Threading; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +[Shared] +[ExportCSharpVisualBasicStatelessLspService(typeof(AutoLoadProjectsInitializer))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class AutoLoadProjectsInitializer( + LanguageServerProjectSystem projectSystem, + ILoggerFactory loggerFactory, + ServerConfiguration serverConfiguration, + IGlobalOptionService globalOptionService) : ILspService, IOnInitialized +{ + private readonly ILogger _logger = loggerFactory.CreateLogger(); + + public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + { + if (!serverConfiguration.AutoLoadProjects) + { + return; + } + + var isUsingDevKit = globalOptionService.GetOption(LspOptionsStorage.LspUsingDevkitFeatures); + Contract.ThrowIfTrue(isUsingDevKit, "Auto load projects is not supported when using DevKit."); + + var initializeParams = context.GetRequiredService().TryGetInitializeParams(); + Contract.ThrowIfNull(initializeParams, "Initialize params should be set during initialization."); + + var workspaceFolders = initializeParams.WorkspaceFolders; + if (workspaceFolders is null || workspaceFolders.Length == 0) + { + _logger.LogWarning("No workspace folders provided during initialization; could not auto load projects."); + return; + } + + var (isLoadingDisabled, solutionPath) = TryGetVSCodeSolutionSettings(workspaceFolders, _logger); + if (isLoadingDisabled) + { + _logger.LogInformation("Using VS Code settings to disable auto loading solution on startup."); + return; + } + else if (solutionPath is not null) + { + _logger.LogInformation("Using VS Code settings to auto load solution {SolutionFile}", solutionPath); + await StartAndReportProgressAsync(() => projectSystem.OpenSolutionAsync(solutionPath)); + return; + } + + // If there's a single workspace folder with a single solution file at the root, load that solution. + if (workspaceFolders.Length == 1) + { + var folder = workspaceFolders[0]; + if (TryGetFolderPath(folder, _logger, out var folderPath)) + { + var solutionFiles = Directory.EnumerateFiles(folderPath, "*.sln", SearchOption.TopDirectoryOnly) + .Concat(Directory.EnumerateFiles(folderPath, "*.slnx", SearchOption.TopDirectoryOnly)) + .ToArray(); + + if (solutionFiles.Length == 1) + { + _logger.LogInformation("Found single solution file {SolutionFile} to auto load", solutionFiles[0]); + await StartAndReportProgressAsync(() => projectSystem.OpenSolutionAsync(solutionFiles[0])); + return; + } + } + } + + using var _ = ArrayBuilder.GetInstance(out var projectFiles); + foreach (var folder in workspaceFolders) + { + _logger.LogTrace("Searching for projects to load in workspace folder: {FolderUri}", folder.DocumentUri); + if (TryGetFolderPath(folder, _logger, out var folderPath)) + { + projectFiles.AddRange(Directory.EnumerateFiles(folderPath, "*.csproj", SearchOption.AllDirectories)); + } + } + + _logger.LogInformation("Discovered {count} projects to auto load", projectFiles.Count); + + await StartAndReportProgressAsync(() => projectSystem.OpenProjectsAsync(projectFiles.ToImmutable())); + + async Task StartAndReportProgressAsync(Func loadOperation) + { + var workDoneProgressManager = context.GetRequiredLspService(); + + // We will await for the client to know that we are starting work... + var progressReporter = await workDoneProgressManager.CreateWorkDoneProgressAsync(reportProgressToClient: true, cancellationToken); + + // ...but we'll fire-and-forget for the actual loading. Pass CancellationToken.None since we want to ensure the progressReporter is always disposed. + Task.Run(async () => + { + try + { + await loadOperation(); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex)) + { + } + finally + { + progressReporter.Dispose(); + } + }, CancellationToken.None).Forget(); + } + } + + internal static bool TryGetFolderPath(WorkspaceFolder folder, ILogger logger, [NotNullWhen(returnValue: true)] out string? folderPath) + { + if (folder.DocumentUri.ParsedUri is null || folder.DocumentUri.ParsedUri.Scheme != Uri.UriSchemeFile) + { + logger.LogWarning("Workspace folder {FolderUri} is not a file URI, skipping.", folder.DocumentUri); + folderPath = null; + return false; + } + + folderPath = ProtocolConversions.GetDocumentFilePathFromUri(folder.DocumentUri.ParsedUri); + if (!Directory.Exists(folderPath)) + { + logger.LogWarning("Workspace folder path {FolderPath} does not exist, skipping.", folderPath); + return false; + } + + return true; + } + + internal static (bool isLoadingDisabled, string? solutionPath) TryGetVSCodeSolutionSettings(WorkspaceFolder[] workspaceFolders, ILogger logger) + { + foreach (var folder in workspaceFolders) + { + logger.LogTrace("Searching for VS Code settings to load in workspace folder: {FolderUri}", folder.DocumentUri); + + if (TryGetFolderPath(folder, logger, out var folderPath) + && VSCodeSettings.TryRead(Path.Combine(folderPath, ".vscode", "settings.json"), logger, out var settings) + && settings.TryGetStringSetting(VSCodeSettings.Names.DefaultSolution) is { Length: > 0 } defaultSolution) + { + var isLoadingDisabled = string.Equals(defaultSolution, "disable", StringComparison.Ordinal); + if (isLoadingDisabled) + { + if (workspaceFolders.Length == 1) + return (isLoadingDisabled: true, solutionPath: null); + + continue; + } + + var solutionPath = Path.IsPathRooted(defaultSolution) + ? defaultSolution + : Path.GetFullPath(Path.Combine(folderPath, defaultSolution)); + + if (!File.Exists(solutionPath)) + { + logger.LogWarning("Default solution path {SolutionPath} does not exist, skipping.", solutionPath); + continue; + } + + return (isLoadingDisabled: false, solutionPath); + } + } + + return (isLoadingDisabled: false, solutionPath: null); + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DefaultFileChangeWatcher.FileChangeContext.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DefaultFileChangeWatcher.FileChangeContext.cs new file mode 100644 index 000000000000..42a0ddfa5afc --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DefaultFileChangeWatcher.FileChangeContext.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis.ProjectSystem; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; + +internal sealed partial class DefaultFileChangeWatcher +{ + /// + /// A file change context that tracks watched directories and files. + /// + /// + /// Each context tracks which root watchers it has acquired, subscribing to their events + /// and unsubscribing when disposed. It also tracks individual files being watched outside + /// of directory watches. + /// + internal sealed class FileChangeContext : IFileChangeContext, IEventRaiser + { + private readonly DefaultFileChangeWatcher _owner; + private readonly ImmutableArray _watchedDirectories; + private readonly ImmutableArray>> _fileSystemWatchersForWatchedDirectories; + private bool _disposed = false; + + public FileChangeContext(DefaultFileChangeWatcher owner, ImmutableArray watchedDirectories) + { + _owner = owner; + + var watchedRootPaths = new HashSet(s_pathStringComparer); + var fileSystemWatchersForWatchedDirectoriesBuilder = ImmutableArray.CreateBuilder>>(); + var watchedDirectoryBuilder = ImmutableArray.CreateBuilder(watchedDirectories.Length); + foreach (var watchedDirectory in watchedDirectories) + { + if (!Directory.Exists(watchedDirectory.Path)) + continue; + + watchedDirectoryBuilder.Add(watchedDirectory); + + var rootPath = Path.GetPathRoot(watchedDirectory.Path)!; + if (!watchedRootPaths.Add(rootPath)) + continue; + + var rootWatcher = _owner.GetOrCreateSharedWatcher(rootPath); + fileSystemWatchersForWatchedDirectoriesBuilder.Add(rootWatcher); + } + + _watchedDirectories = watchedDirectoryBuilder.ToImmutable(); + _fileSystemWatchersForWatchedDirectories = fileSystemWatchersForWatchedDirectoriesBuilder.ToImmutable(); + + // Attach watchers after fields are assigned to avoid race conditions where events + // fire before _watchedDirectories is initialized. + foreach (var rootWatcher in _fileSystemWatchersForWatchedDirectories) + AttachWatcher(this, rootWatcher); + } + + public event EventHandler? FileChanged; + + void IEventRaiser.RaiseEvent(object? sender, FileSystemEventArgs e) + { + if (_watchedDirectories.IsEmpty) + return; + + if (WatchedDirectory.FilePathCoveredByWatchedDirectories(_watchedDirectories, e.FullPath, s_pathStringComparison)) + { + FileChanged?.Invoke(this, e.FullPath); + + // On Windows we only get a renamed event instead of separate delete/create events, so also raise + // a change event for the old file path. + if (e is RenamedEventArgs re && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + FileChanged?.Invoke(this, re.OldFullPath); + } + } + + public IWatchedFile EnqueueWatchingFile(string filePath) + { + // If this path is already covered by one of our directory watchers, nothing further to do + if (WatchedDirectory.FilePathCoveredByWatchedDirectories(_watchedDirectories, filePath, s_pathStringComparison)) + return NoOpWatchedFile.Instance; + + // If this path doesn't have a valid root, we can't watch it + var rootPath = Path.GetPathRoot(filePath); + if (string.IsNullOrEmpty(rootPath) || !Directory.Exists(rootPath)) + return NoOpWatchedFile.Instance; + + var rootWatcher = _owner.GetOrCreateSharedWatcher(rootPath); + return new IndividualWatchedFile(this, filePath, rootWatcher); + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, true) == false) + { + foreach (var rootWatcher in _fileSystemWatchersForWatchedDirectories) + DetachAndDisposeWatcher(this, rootWatcher); + } + } + + private sealed class IndividualWatchedFile : IWatchedFile, IEventRaiser + { + private readonly FileChangeContext _context; + private readonly string _filePath; + private readonly IReferenceCountedDisposable> _watcher; + private bool _disposed = false; + + public IndividualWatchedFile(FileChangeContext context, string filePath, IReferenceCountedDisposable> watcher) + { + _context = context; + _filePath = filePath; + _watcher = watcher; + + AttachWatcher(this, _watcher); + } + + void IEventRaiser.RaiseEvent(object? sender, FileSystemEventArgs e) + { + if (e.FullPath.Equals(_filePath, s_pathStringComparison)) + { + _context.FileChanged?.Invoke(this, e.FullPath); + } + else if (e is RenamedEventArgs re && RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + re.OldFullPath.Equals(_filePath, s_pathStringComparison)) + { + // On Windows we only get a renamed event instead of separate delete/create events, so check + // whether the old file path matches. + _context.FileChanged?.Invoke(this, re.OldFullPath); + } + } + + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, true) == false) + DetachAndDisposeWatcher(this, _watcher); + } + } + + internal static class TestAccessor + { + public static ImmutableArray>> GetRootFileWatchers(FileChangeContext context) + => context._fileSystemWatchersForWatchedDirectories; + } + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DefaultFileChangeWatcher.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DefaultFileChangeWatcher.cs new file mode 100644 index 000000000000..6e0f5f236037 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DefaultFileChangeWatcher.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis.ProjectSystem; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; + +/// +/// An implementation of that is built atop the framework . This is used if we can't +/// use the LSP one. +/// +/// +/// This implementation creates one per root drive and uses filtering to route file +/// change events to the appropriate watchers. The watchers are shared between all +/// instances and are disposed when all contexts using them have been disposed. +/// +internal sealed partial class DefaultFileChangeWatcher : IFileChangeWatcher +{ + private static readonly StringComparer s_pathStringComparer = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? StringComparer.OrdinalIgnoreCase + : StringComparer.Ordinal; + private static readonly StringComparison s_pathStringComparison = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + + private readonly ReferenceCountedDisposableCache _sharedRootWatchers = new(s_pathStringComparer); + + public IFileChangeContext CreateContext(ImmutableArray watchedDirectories) + => new FileChangeContext(this, watchedDirectories); + + private IReferenceCountedDisposable> GetOrCreateSharedWatcher(string rootPath) + { + var rootWatcher = _sharedRootWatchers.GetOrCreate(rootPath, static (key, _) => new FileSystemWatcher(key), arg: null); + rootWatcher.Target.Value.IncludeSubdirectories = true; + rootWatcher.Target.Value.EnableRaisingEvents = true; + return rootWatcher; + } + + private static void AttachWatcher(IEventRaiser eventRaiser, IReferenceCountedDisposable> watcher) + { + watcher.Target.Value.Changed += eventRaiser.RaiseEvent; + watcher.Target.Value.Created += eventRaiser.RaiseEvent; + watcher.Target.Value.Deleted += eventRaiser.RaiseEvent; + watcher.Target.Value.Renamed += eventRaiser.RaiseEvent; + } + + private static void DetachAndDisposeWatcher(IEventRaiser eventRaiser, IReferenceCountedDisposable> watcher) + { + watcher.Target.Value.Changed -= eventRaiser.RaiseEvent; + watcher.Target.Value.Created -= eventRaiser.RaiseEvent; + watcher.Target.Value.Deleted -= eventRaiser.RaiseEvent; + watcher.Target.Value.Renamed -= eventRaiser.RaiseEvent; + watcher.Dispose(); + } + + internal interface IEventRaiser + { + void RaiseEvent(object? sender, FileSystemEventArgs e); + } + + internal static class TestAccessor + { + public static IEnumerable GetWatchedRootPaths(DefaultFileChangeWatcher watcher) + => ReferenceCountedDisposableCache.TestAccessor.GetCacheKeys(watcher._sharedRootWatchers); + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DelegatingFileChangeWatcher.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DelegatingFileChangeWatcher.cs index b32ec7ef66d3..4a9b015097fc 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DelegatingFileChangeWatcher.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DelegatingFileChangeWatcher.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; /// /// A MEF export for . This checks if we're able to create an if the client supports file watching. If we do, we create that and delegate to it. -/// Otherwise we use a . +/// Otherwise we use a . /// /// /// LSP clients don't always support file watching; this allows us to be flexible and use it when we can, but fall back @@ -38,7 +38,7 @@ internal sealed class DelegatingFileChangeWatcher( return new LspFileChangeWatcher(instance, asynchronousOperationListenerProvider); loggerFactory.CreateLogger().LogWarning("We are unable to use LSP file watching; falling back to our in-process watcher."); - return new SimpleFileChangeWatcher(); + return new DefaultFileChangeWatcher(); }); public IFileChangeContext CreateContext(ImmutableArray watchedDirectories) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs index 24f2b0fbd2bf..fd3d96c02008 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs @@ -62,7 +62,7 @@ private sealed class FileChangeContext : IFileChangeContext /// /// The list of file paths we're watching manually that were outside the directories being watched. The count in this case counts - /// the number of + /// the number of watchers registered for each file. /// private readonly Dictionary _watchedFiles = new(s_stringComparer); private static readonly StringComparer s_stringComparer = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.cs deleted file mode 100644 index 06d20ab99a7b..000000000000 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.cs +++ /dev/null @@ -1,136 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.ProjectSystem; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; - -/// -/// A trivial implementation of that is built atop the framework . This is used if we can't -/// use the LSP one. -/// -/// -/// This implementation is not remotely efficient, but is available as a fallback implementation. If this needs to regularly be used, then this should get some improvements. -/// -internal sealed class SimpleFileChangeWatcher : IFileChangeWatcher -{ - public IFileChangeContext CreateContext(ImmutableArray watchedDirectories) - => new FileChangeContext(watchedDirectories); - - private sealed class FileChangeContext : IFileChangeContext - { - private readonly ImmutableArray _watchedDirectories; - - /// - /// The directory watchers for the . - /// - private readonly ImmutableArray _directoryFileSystemWatchers; - private readonly ConcurrentSet _individualWatchedFiles = []; - - public FileChangeContext(ImmutableArray watchedDirectories) - { - var watchedDirectoriesBuilder = ImmutableArray.CreateBuilder(watchedDirectories.Length); - var watcherBuilder = ImmutableArray.CreateBuilder(watchedDirectories.Length); - - foreach (var watchedDirectory in watchedDirectories) - { - // If the directory doesn't exist, we can't create a watcher for changes inside of it. In this case, we'll just skip this as a directory - // to watch; any requests for a watch within that directory will still create a one-off watcher for that specific file. That's not likely - // to be an issue in practice: directories that are missing would be things like global reference directories -- if it's not there, we - // probably won't ever see a watch for a file under there later anyways. - if (Directory.Exists(watchedDirectory.Path)) - { - var watcher = new FileSystemWatcher(watchedDirectory.Path); - watcher.IncludeSubdirectories = true; - - foreach (var filter in watchedDirectory.ExtensionFilters) - watcher.Filters.Add('*' + filter); - - watcher.Changed += RaiseEvent; - watcher.Created += RaiseEvent; - watcher.Deleted += RaiseEvent; - watcher.Renamed += RaiseEvent; - - watcher.EnableRaisingEvents = true; - - watchedDirectoriesBuilder.Add(watchedDirectory); - watcherBuilder.Add(watcher); - } - } - - _watchedDirectories = watchedDirectoriesBuilder.ToImmutable(); - _directoryFileSystemWatchers = watcherBuilder.ToImmutable(); - } - - public event EventHandler? FileChanged; - - public IWatchedFile EnqueueWatchingFile(string filePath) - { - // If this path is already covered by one of our directory watchers, nothing further to do - if (WatchedDirectory.FilePathCoveredByWatchedDirectories(_watchedDirectories, filePath, StringComparison.Ordinal)) - return NoOpWatchedFile.Instance; - - var individualWatchedFile = new IndividualWatchedFile(filePath, this); - _individualWatchedFiles.Add(individualWatchedFile); - return individualWatchedFile; - } - - private void RaiseEvent(object sender, FileSystemEventArgs e) - { - FileChanged?.Invoke(this, e.FullPath); - } - - public void Dispose() - { - foreach (var directoryWatcher in _directoryFileSystemWatchers) - directoryWatcher.Dispose(); - } - - private sealed class IndividualWatchedFile : IWatchedFile - { - private readonly FileChangeContext _context; - private readonly FileSystemWatcher? _watcher; - - public IndividualWatchedFile(string filePath, FileChangeContext context) - { - _context = context; - - // We always must create a watch on an entire directory, so create that, filtered to the single file name - var directoryPath = Path.GetDirectoryName(filePath); - - // TODO: support missing directories properly - if (Directory.Exists(directoryPath)) - { - _watcher = new FileSystemWatcher(directoryPath, Path.GetFileName(filePath)); - _watcher.IncludeSubdirectories = false; - - _watcher.Changed += _context.RaiseEvent; - _watcher.Created += _context.RaiseEvent; - _watcher.Deleted += _context.RaiseEvent; - _watcher.Renamed += _context.RaiseEvent; - - _watcher.EnableRaisingEvents = true; - } - else - { - _watcher = null; - } - } - - public void Dispose() - { - if (_context._individualWatchedFiles.Remove(this) && _watcher != null) - { - _watcher.Changed -= _context.RaiseEvent; - _watcher.Created -= _context.RaiseEvent; - _watcher.Deleted -= _context.RaiseEvent; - _watcher.Renamed -= _context.RaiseEvent; - _watcher.Dispose(); - } - } - } - } -} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs index 1cbdb4809236..ff6a62c049c2 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs @@ -28,6 +28,7 @@ internal abstract class LanguageServerProjectLoader private readonly IFileChangeWatcher _fileChangeWatcher; protected readonly IGlobalOptionService GlobalOptionService; protected readonly ILoggerFactory LoggerFactory; + protected readonly IAsynchronousOperationListener Listener; private readonly ILogger _logger; private readonly ProjectLoadTelemetryReporter _projectLoadTelemetryReporter; private readonly IBinLogPathProvider _binLogPathProvider; @@ -100,6 +101,7 @@ protected LanguageServerProjectLoader( _fileChangeWatcher = fileChangeWatcher; GlobalOptionService = globalOptionService; LoggerFactory = loggerFactory; + Listener = listenerProvider.GetListener(FeatureAttribute.Workspace); _logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectLoader)); _projectLoadTelemetryReporter = projectLoadTelemetry; _binLogPathProvider = binLogPathProvider; @@ -111,7 +113,7 @@ protected LanguageServerProjectLoader( TimeSpan.FromMilliseconds(100), ReloadProjectsAsync, ProjectToLoad.Comparer, - listenerProvider.GetListener(FeatureAttribute.Workspace), + Listener, CancellationToken.None); // TODO: do we need to introduce a shutdown cancellation token for this? } @@ -158,7 +160,12 @@ private async ValueTask ReloadProjectsAsync(ImmutableSegmentedList(), + globalMSBuildProperties: AdditionalProperties, + binaryLogPathProvider: _binLogPathProvider, + loggerFactory: LoggerFactory); + var toastErrorReporter = new ToastErrorReporter(); try @@ -191,10 +198,12 @@ private async ValueTask ReloadProjectsAsync(ImmutableSegmentedList ProjectFileInfos { get; init; } + public required ImmutableArray DiagnosticLogItems { get; init; } public required ProjectSystemProjectFactory ProjectFactory { get; init; } public required bool IsFileBasedProgram { get; init; } public required bool IsMiscellaneousFile { get; init; } + public required bool HasAllInformation { get; init; } public required BuildHostProcessKind PreferredBuildHostKind { get; init; } public required BuildHostProcessKind ActualBuildHostKind { get; init; } } @@ -204,30 +213,11 @@ internal sealed record RemoteProjectLoadResult protected abstract Task TryLoadProjectInMSBuildHostAsync( BuildHostProcessManager buildHostProcessManager, string projectPath, CancellationToken cancellationToken); - /// Called after a project is unloaded to allow the subtype to clean up any resources associated with the project. - /// - /// Note that this refers to unloading of the project on the project-system level. - /// So, for example, changing the target frameworks of a project, or transitioning between - /// "file-based program" and "true miscellaneous file", will not result in this being called. - /// - protected abstract ValueTask OnProjectUnloadedAsync(string projectFilePath); - - /// - /// Called when transitioning from a primordial project to loaded targets. - /// Subclasses can override this to transfer documents or perform other operations before the primordial project is removed. - /// - protected abstract ValueTask TransitionPrimordialProjectToLoaded_NoLockAsync( - string projectPath, - ProjectSystemProjectFactory primordialProjectFactory, - ProjectId primordialProjectId, - CancellationToken cancellationToken); - /// True if the project needs a NuGet restore, false otherwise. private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastErrorReporter toastErrorReporter, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken) { BuildHostProcessKind? preferredBuildHostKindThatWeDidNotGet = null; var projectPath = projectToLoad.Path; - Contract.ThrowIfFalse(PathUtilities.IsAbsolute(projectPath)); // Before doing any work, check if the project has already been unloaded. using (await _gate.DisposableWaitAsync(cancellationToken)) @@ -243,21 +233,21 @@ private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr var remoteProjectLoadResult = await TryLoadProjectInMSBuildHostAsync(buildHostProcessManager, projectPath, cancellationToken); if (remoteProjectLoadResult is null) { - // Note that this is a fairly common condition, e.g. for VB projects. - // In the file-based programs primordial case, no 'LoadedProject' is produced for the project, - // and therefore no reloading is performed for it after failing to load it once (in this code path). - _logger.LogWarning($"Unable to load project '{projectPath}'."); + // Example cases where this might occur: + // - Loading VB projects + // - Reloading file-based app projects, where edits were performed to e.g. delete all `#:` directives, + // making the file no longer a file-based app entry point. + _logger.LogDebug("Reload of '{projectPath}' was canceled.", projectPath); return false; } - var remoteProjectFile = remoteProjectLoadResult.ProjectFile; var projectFactory = remoteProjectLoadResult.ProjectFactory; var isMiscellaneousFile = remoteProjectLoadResult.IsMiscellaneousFile; var preferredBuildHostKind = remoteProjectLoadResult.PreferredBuildHostKind; if (preferredBuildHostKind != remoteProjectLoadResult.ActualBuildHostKind) preferredBuildHostKindThatWeDidNotGet = preferredBuildHostKind; - var diagnosticLogItems = await remoteProjectFile.GetDiagnosticLogItemsAsync(cancellationToken); + var diagnosticLogItems = remoteProjectLoadResult.DiagnosticLogItems; if (diagnosticLogItems.Any(item => item.Kind is DiagnosticLogItemKind.Error)) { await LogDiagnosticsAsync(diagnosticLogItems); @@ -265,7 +255,7 @@ private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr return false; } - var loadedProjectInfos = await remoteProjectFile.GetProjectFileInfosAsync(cancellationToken); + var loadedProjectInfos = remoteProjectLoadResult.ProjectFileInfos; // The out-of-proc build host supports more languages than we may actually have Workspace binaries for, so ensure we can actually process that // language in-process. @@ -293,7 +283,7 @@ private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr var (target, targetAlreadyExists) = await GetOrCreateProjectTargetAsync(previousProjectTargets, projectFactory, loadedProjectInfo); newProjectTargetsBuilder.Add(target); - var (outputKind, metadataReferences, targetNeedsRestore) = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, isMiscellaneousFile, _logger); + var (outputKind, metadataReferences, targetNeedsRestore) = await target.UpdateWithNewProjectInfoAsync(loadedProjectInfo, isMiscellaneousFile, remoteProjectLoadResult.HasAllInformation, _logger); needsRestore |= targetNeedsRestore; if (!targetAlreadyExists) { @@ -324,10 +314,12 @@ private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr await _projectLoadTelemetryReporter.ReportProjectLoadTelemetryAsync(telemetryInfos, projectToLoad, cancellationToken); } - if (currentLoadState is ProjectLoadState.Primordial(var primordialProjectFactory, var projectId)) + if (currentLoadState is ProjectLoadState.Primordial primordial) { - // Transition from primordial to loaded state - await TransitionPrimordialProjectToLoaded_NoLockAsync(projectPath, primordialProjectFactory, projectId, cancellationToken); + // Remove the primordial project from the workspace now that the design-time build has produced real targets. + await primordial.PrimordialProjectFactory.ApplyChangeToWorkspaceAsync( + workspace => workspace.OnProjectRemoved(primordial.PrimordialProjectId), + cancellationToken); } // At this point we expect that all the loaded projects are now in the project factory returned, and any previous ones have been removed. @@ -337,7 +329,6 @@ private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr _loadedProjects[projectPath] = new ProjectLoadState.LoadedTargets(newProjectTargets); } - diagnosticLogItems = await remoteProjectFile.GetDiagnosticLogItemsAsync(cancellationToken); if (diagnosticLogItems.Any()) { await LogDiagnosticsAsync(diagnosticLogItems); @@ -373,7 +364,9 @@ private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr var projectCreationInfo = new ProjectSystemProjectCreationInfo { AssemblyName = projectSystemName, - FilePath = projectPath, + // Note: the project file might be for a virtual file that doesn't exist on disk. + // In this case, we don't want to pass its path through here, as this will result in trying to take file system timestamps for it, watch it for changes, etc. + FilePath = PathUtilities.IsAbsolute(projectPath) && File.Exists(projectPath) ? projectPath : null, CompilationOutputAssemblyFilePath = loadedProjectInfo.IntermediateOutputFilePath, }; @@ -381,7 +374,8 @@ private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastEr projectSystemName, loadedProjectInfo.Language, projectCreationInfo, - _workspaceFactory.ProjectSystemHostInfo); + _workspaceFactory.ProjectSystemHostInfo, + cancellationToken).ConfigureAwait(false); var loadedProject = new LoadedProject(projectSystemProject, projectFactory, _fileChangeWatcher, _workspaceFactory.TargetFrameworkManager); loadedProject.NeedsReload += (_, _) => @@ -412,56 +406,47 @@ async Task LogDiagnosticsAsync(ImmutableArray diagnosticLogIt } } - protected async ValueTask IsProjectLoadedAsync(string projectPath, CancellationToken cancellationToken) + protected async ValueTask GetOrLoadProjectAsync(string projectPath, ProjectSystemProjectFactory primordialProjectFactory, Func createPrimordialProjectInfo, bool doDesignTimeBuild) { - using (await _gate.DisposableWaitAsync(cancellationToken)) + using (await _gate.DisposableWaitAsync(CancellationToken.None)) { - return _loadedProjects.ContainsKey(projectPath); - } - } + if (_loadedProjects.TryGetValue(projectPath, out var existingState)) + { + // Note: this generally only happens if we fall through to the "add to misc workspace" path, + // and we lose a race to begin loading the miscellaneous file project. + return LookupExistingProject(existingState); + } - /// - /// Executes an async action with access to the loaded project state under the _gate. - /// This allows subclasses to safely query or modify project state. - /// - protected async ValueTask ExecuteUnderGateAsync(Func, ValueTask> action, CancellationToken cancellationToken) - { - using (await _gate.DisposableWaitAsync(cancellationToken)) - { - return await action(_loadedProjects); - } - } + var primordialProjectInfo = createPrimordialProjectInfo(primordialProjectFactory); + primordialProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(primordialProjectInfo)); + _loadedProjects.Add(projectPath, new ProjectLoadState.Primordial(primordialProjectFactory, primordialProjectInfo.Id)); + if (doDesignTimeBuild) + _projectsToReload.AddWork(new ProjectToLoad(projectPath, ProjectGuid: null, ReportTelemetry: true)); - /// - protected async ValueTask BeginLoadingProjectWithPrimordialAsync(string projectPath, ProjectSystemProjectFactory primordialProjectFactory, ProjectId primordialProjectId, bool doDesignTimeBuild) - { - using (await _gate.DisposableWaitAsync(CancellationToken.None)) - { - BeginLoadingProjectWithPrimordial_NoLock(projectPath, primordialProjectFactory, primordialProjectId, doDesignTimeBuild); + return primordialProjectFactory.Workspace.CurrentSolution.GetRequiredProject(primordialProjectInfo.Id); } - } - /// - /// Begins loading a project with an associated primordial project. Must not be called for a project which has already begun loading. - /// - /// - /// If , initiates a design-time build now, and starts file watchers to repeat the design-time build on relevant changes. - /// If , only tracks the primordial project. - /// - protected void BeginLoadingProjectWithPrimordial_NoLock(string projectPath, ProjectSystemProjectFactory primordialProjectFactory, ProjectId primordialProjectId, bool doDesignTimeBuild) - { - // If this project has already begun loading, we need to throw. - // This is because we can't ensure that the workspace and project system will remain in a consistent state after this call. - // For example, there could be a need for the project system to track both a primordial project and list of loaded targets, which we don't support. - if (_loadedProjects.ContainsKey(projectPath)) + Project? LookupExistingProject(ProjectLoadState loadState) { - Contract.Fail($"Cannot begin loading project '{projectPath}' because it has already begun loading."); - } + if (loadState is ProjectLoadState.Primordial primordial) + { + return primordial.PrimordialProjectFactory.Workspace.CurrentSolution.GetRequiredProject(primordial.PrimordialProjectId); + } + else if (loadState is ProjectLoadState.LoadedTargets loadedTargets) + { + var target = loadedTargets.LoadedProjectTargets.FirstOrDefault(); + if (target is null) + { + _logger.LogWarning("Could not get a project for '{projectPath}' because it loaded with no targets", projectPath); + return null; + } - _loadedProjects.Add(projectPath, new ProjectLoadState.Primordial(primordialProjectFactory, primordialProjectId)); - if (doDesignTimeBuild) - { - _projectsToReload.AddWork(new ProjectToLoad(projectPath, ProjectGuid: null, ReportTelemetry: true)); + return target.ProjectFactory.Workspace.CurrentSolution.GetRequiredProject(target.ProjectId); + } + else + { + throw ExceptionUtilities.UnexpectedValue(loadState); + } } } @@ -485,36 +470,87 @@ protected async Task BeginLoadingProjectAsync(string projectPath, string? projec protected Task WaitForProjectsToFinishLoadingAsync() => _projectsToReload.WaitUntilCurrentBatchCompletesAsync(); - protected async ValueTask TryUnloadProjectAsync(string projectPath) + /// Unloads all projects associated with this project loader. + internal async ValueTask UnloadAllProjectsAsync() { using (await _gate.DisposableWaitAsync(CancellationToken.None)) { - if (!_loadedProjects.Remove(projectPath, out var loadState)) + foreach (var key in _loadedProjects.Keys) { - // It is common to be called with a path to a project which is already not loaded. - // In this case, we should do nothing. - return false; + // Note that .NET supports removing dictionary entries while enumerating + var removed = await TryUnloadProject_NoLockAsync(key); + Contract.ThrowIfFalse(removed); // We obtained lock before enumerating, how was this already removed? } + } + } + + internal async ValueTask TryUnloadProjectAsync(string projectPath, ProjectSystemProjectFactory? fromProjectFactory = null) + { + using (await _gate.DisposableWaitAsync(CancellationToken.None)) + { + return await TryUnloadProject_NoLockAsync(projectPath, fromProjectFactory); + } + } + + private async ValueTask TryUnloadProject_NoLockAsync(string projectPath, ProjectSystemProjectFactory? fromProjectFactory = null) + { + // Caller can specify to only unload a project if it uses a specific project factory. + if (fromProjectFactory != null && !UsesProjectFactory(fromProjectFactory)) + { + return false; + } + + if (!_loadedProjects.Remove(projectPath, out var loadState)) + { + // It is common to be called with a path to a project which is already not loaded. + // In this case, we should do nothing. + return false; + } - if (loadState is ProjectLoadState.Primordial(var projectFactory, var projectId)) + if (loadState is ProjectLoadState.Primordial(var projectFactory, var projectId)) + { + await projectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId)); + } + else if (loadState is ProjectLoadState.LoadedTargets(var existingProjects)) + { + foreach (var existingProject in existingProjects) { - await projectFactory.ApplyChangeToWorkspaceAsync(workspace => workspace.OnProjectRemoved(projectId)); + // Disposing a LoadedProject unloads it and removes it from the workspace. + existingProject.Dispose(); } - else if (loadState is ProjectLoadState.LoadedTargets(var existingProjects)) + } + else + { + throw ExceptionUtilities.UnexpectedValue(loadState); + } + + return true; + + bool UsesProjectFactory(ProjectSystemProjectFactory fromProjectFactory) + { + if (_loadedProjects.TryGetValue(projectPath, out var loadState1)) { - foreach (var existingProject in existingProjects) + if (loadState1 is ProjectLoadState.Primordial(var projectFactory1, _)) { - // Disposing a LoadedProject unloads it and removes it from the workspace. - existingProject.Dispose(); + if (projectFactory1 == fromProjectFactory) + return true; + } + else if (loadState1 is ProjectLoadState.LoadedTargets(var existingProjects)) + { + // Assumption: All 'existingProject' items will use the same project factory. + foreach (var existingProject in existingProjects) + { + if (existingProject.ProjectFactory == fromProjectFactory) + return true; + } + } + else + { + throw ExceptionUtilities.UnexpectedValue(loadState1); } } - else - { - throw ExceptionUtilities.UnexpectedValue(loadState); - } - } - await OnProjectUnloadedAsync(projectPath); - return true; + return false; + } } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs index 995ec7af0d9f..6eba02555588 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.Composition; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; @@ -48,7 +49,7 @@ public LanguageServerProjectSystem( _logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectSystem)); _hostProjectFactory = workspaceFactory.HostProjectFactory; var workspace = workspaceFactory.HostWorkspace; - _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(workspace.CurrentSolution.Services, new DiagnosticReporter(workspace)); + _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(new DiagnosticReporter(workspace)); } public async Task OpenSolutionAsync(string solutionFilePath) @@ -81,6 +82,7 @@ public async Task OpenProjectsAsync(ImmutableArray projectFilePaths) protected override async Task TryLoadProjectInMSBuildHostAsync( BuildHostProcessManager buildHostProcessManager, string projectPath, CancellationToken cancellationToken) { + Contract.ThrowIfFalse(PathUtilities.IsAbsolute(projectPath)); if (!_projectFileExtensionRegistry.TryGetLanguageNameFromProjectPath(projectPath, DiagnosticReportingMode.Ignore, out var languageName)) return null; @@ -90,26 +92,14 @@ public async Task OpenProjectsAsync(ImmutableArray projectFilePaths) var loadedFile = await buildHost.LoadProjectFileAsync(projectPath, languageName, cancellationToken); return new RemoteProjectLoadResult { - ProjectFile = loadedFile, + ProjectFileInfos = await loadedFile.GetProjectFileInfosAsync(cancellationToken), + DiagnosticLogItems = await loadedFile.GetDiagnosticLogItemsAsync(cancellationToken), ProjectFactory = _hostProjectFactory, IsFileBasedProgram = false, IsMiscellaneousFile = false, + HasAllInformation = true, PreferredBuildHostKind = preferredBuildHostKind, ActualBuildHostKind = actualBuildHostKind }; } - - protected override async ValueTask OnProjectUnloadedAsync(string projectFilePath) - { - // Nothing else to unload for ordinary projects. - } - - protected override async ValueTask TransitionPrimordialProjectToLoaded_NoLockAsync( - string projectPath, - ProjectSystemProjectFactory primordialProjectFactory, - ProjectId primordialProjectId, - CancellationToken cancellationToken) - { - throw ExceptionUtilities.Unreachable(); - } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs index d373b8aedc89..97d9d079fd68 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs @@ -18,8 +18,10 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; -[Export(typeof(LanguageServerWorkspaceFactory)), Shared] -internal sealed class LanguageServerWorkspaceFactory +[Shared] +[Export(typeof(IHostWorkspaceProvider))] +[Export(typeof(LanguageServerWorkspaceFactory))] +internal sealed class LanguageServerWorkspaceFactory : IHostWorkspaceProvider { private readonly ILogger _logger; private readonly ImmutableArray _solutionLevelAnalyzerPaths; @@ -47,7 +49,9 @@ public LanguageServerWorkspaceFactory( // Create the workspace and set analyzer references for it var workspace = new LanguageServerWorkspace(hostServicesProvider.HostServices, WorkspaceKind.Host); - workspace.SetCurrentSolution(s => s.WithAnalyzerReferences(CreateSolutionLevelAnalyzerReferencesForWorkspace(workspace)), WorkspaceChangeKind.SolutionChanged); + var hostAnalyzerLoaderProvider = workspace.Services.GetRequiredService(); + var analyzerReferences = CreateSolutionLevelAnalyzerReferences(hostAnalyzerLoaderProvider); + workspace.SetCurrentSolution(s => s.WithAnalyzerReferences(analyzerReferences), WorkspaceChangeKind.SolutionChanged); HostProjectFactory = new ProjectSystemProjectFactory( workspace, fileChangeWatcher, static (_, _) => Task.CompletedTask, _ => { }, @@ -55,9 +59,12 @@ public LanguageServerWorkspaceFactory( workspace.ProjectSystemProjectFactory = HostProjectFactory; // https://github.com/dotnet/roslyn/issues/78560: Move this workspace creation to 'FileBasedProgramsWorkspaceProviderFactory'. - // 'CreateSolutionLevelAnalyzerReferencesForWorkspace' needs to be broken out into its own service for us to be able to move this. + // 'CreateSolutionLevelAnalyzerReferences' needs to be broken out into its own service for us to be able to move this. var miscellaneousFilesWorkspace = new LanguageServerWorkspace(hostServicesProvider.HostServices, WorkspaceKind.MiscellaneousFiles); - miscellaneousFilesWorkspace.SetCurrentSolution(s => s.WithAnalyzerReferences(CreateSolutionLevelAnalyzerReferencesForWorkspace(miscellaneousFilesWorkspace)), WorkspaceChangeKind.SolutionChanged); + Contract.ThrowIfFalse( + object.ReferenceEquals(hostAnalyzerLoaderProvider, miscellaneousFilesWorkspace.Services.GetRequiredService()), + "Expected same AnalyzerLoaderProvider to be used for both host and misc workspaces."); + miscellaneousFilesWorkspace.SetCurrentSolution(s => s.WithAnalyzerReferences(analyzerReferences), WorkspaceChangeKind.SolutionChanged); MiscellaneousFilesWorkspaceProjectFactory = new ProjectSystemProjectFactory( miscellaneousFilesWorkspace, fileChangeWatcher, static (_, _) => Task.CompletedTask, _ => { }, CancellationToken.None); @@ -71,6 +78,8 @@ public LanguageServerWorkspaceFactory( } public Workspace HostWorkspace => HostProjectFactory.Workspace; + public Workspace MiscellaneousFilesWorkspace => MiscellaneousFilesWorkspaceProjectFactory.Workspace; + Workspace IHostWorkspaceProvider.Workspace => HostWorkspace; public ProjectSystemProjectFactory HostProjectFactory { get; } public ProjectSystemProjectFactory MiscellaneousFilesWorkspaceProjectFactory { get; } @@ -78,29 +87,30 @@ public LanguageServerWorkspaceFactory( public ProjectSystemHostInfo ProjectSystemHostInfo { get; } public ProjectTargetFrameworkManager TargetFrameworkManager { get; } - public ImmutableArray CreateSolutionLevelAnalyzerReferencesForWorkspace(Workspace workspace) + private ImmutableArray CreateSolutionLevelAnalyzerReferences(IAnalyzerAssemblyLoaderProvider loaderProvider) { - var references = ImmutableArray.CreateBuilder(); - var loaderProvider = workspace.Services.GetRequiredService(); - // Load all analyzers into a fresh shadow copied load context. In the future, if we want to support reloading // of solution-level analyzer references, we should just need to listen for changes to those analyzer paths and // then call back into this method to update the solution accordingly. var analyzerLoader = loaderProvider.CreateNewShadowCopyLoader(); + var references = _solutionLevelAnalyzerPaths + .AsParallel() + .Where(analyzerPath => + { + if (File.Exists(analyzerPath)) + return true; - foreach (var analyzerPath in _solutionLevelAnalyzerPaths) - { - if (File.Exists(analyzerPath)) + _logger.LogWarning($"Solution-level analyzer at {analyzerPath} could not be found."); + return false; + }) + .Select(analyzerPath => { - references.Add(new AnalyzerFileReference(analyzerPath, analyzerLoader)); + var reference = new AnalyzerFileReference(analyzerPath, analyzerLoader); _logger.LogDebug($"Solution-level analyzer at {analyzerPath} added to workspace."); - } - else - { - _logger.LogWarning($"Solution-level analyzer at {analyzerPath} could not be found."); - } - } + return reference; + }) + .ToImmutableArray(); - return references.ToImmutableAndClear(); + return references; } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs index cce080e5590c..1cf8bea30a53 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.Logging; @@ -17,14 +18,14 @@ namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; ///
internal sealed class LoadedProject : IDisposable { - private readonly string _projectFilePath; - private readonly string _projectDirectory; + private readonly string? _projectFilePath; + private readonly string? _projectDirectory; private readonly ProjectSystemProject _projectSystemProject; public ProjectSystemProjectFactory ProjectFactory { get; } private readonly ProjectSystemProjectOptionsProcessor _optionsProcessor; - private readonly IFileChangeContext _sourceFileChangeContext; - private readonly IFileChangeContext _projectFileChangeContext; + private readonly IFileChangeContext? _sourceFileCreatedOrDeletedChangeContext; + private readonly IFileChangeContext? _projectFileChangeContext; private readonly IFileChangeContext _assetsFileChangeContext; private readonly ProjectTargetFrameworkManager _targetFrameworkManager; @@ -43,7 +44,6 @@ internal sealed class LoadedProject : IDisposable public LoadedProject(ProjectSystemProject projectSystemProject, ProjectSystemProjectFactory projectFactory, IFileChangeWatcher fileWatcher, ProjectTargetFrameworkManager targetFrameworkManager) { - Contract.ThrowIfNull(projectSystemProject.FilePath); _projectFilePath = projectSystemProject.FilePath; _projectSystemProject = projectSystemProject; @@ -53,21 +53,28 @@ public LoadedProject(ProjectSystemProject projectSystemProject, ProjectSystemPro // We'll watch the directory for all source file changes // TODO: we only should listen for add/removals here, but we can't specify such a filter now - _projectDirectory = Path.GetDirectoryName(_projectFilePath)!; - - _sourceFileChangeContext = fileWatcher.CreateContext([new(_projectDirectory, [".cs", ".cshtml", ".razor"])]); - _sourceFileChangeContext.FileChanged += SourceFileChangeContext_FileChanged; + _projectDirectory = Path.GetDirectoryName(_projectFilePath); + if (_projectDirectory is not null) + { + _sourceFileCreatedOrDeletedChangeContext = fileWatcher.CreateContext([new(_projectDirectory, [".cs", ".cshtml", ".razor"])]); + _sourceFileCreatedOrDeletedChangeContext.FileChanged += SourceFileCreatedOrDeletedChangeContext_FileChanged; + } - _projectFileChangeContext = fileWatcher.CreateContext([]); - _projectFileChangeContext.FileChanged += ProjectFileChangeContext_FileChanged; - _projectFileChangeContext.EnqueueWatchingFile(_projectFilePath); + if (_projectFilePath is not null) + { + _projectFileChangeContext = fileWatcher.CreateContext([]); + _projectFileChangeContext.FileChanged += ProjectFileChangeContext_FileChanged; + _projectFileChangeContext.EnqueueWatchingFile(_projectFilePath); + } _assetsFileChangeContext = fileWatcher.CreateContext([]); _assetsFileChangeContext.FileChanged += AssetsFileChangeContext_FileChanged; } - private void SourceFileChangeContext_FileChanged(object? sender, string filePath) + private void SourceFileCreatedOrDeletedChangeContext_FileChanged(object? sender, string filePath) { + Contract.ThrowIfNull(_projectDirectory); + var matchers = _mostRecentFileMatchers?.Value; if (matchers is null) { @@ -128,13 +135,13 @@ private void AssetsFileChangeContext_FileChanged(object? sender, string filePath ///
public void Dispose() { - _sourceFileChangeContext.Dispose(); - _projectFileChangeContext.Dispose(); + _sourceFileCreatedOrDeletedChangeContext?.Dispose(); + _projectFileChangeContext?.Dispose(); _optionsProcessor.Dispose(); _projectSystemProject.RemoveFromWorkspace(); } - public async ValueTask<(OutputKind OutputKind, ImmutableArray MetadataReferences, bool NeedsRestore)> UpdateWithNewProjectInfoAsync(ProjectFileInfo newProjectInfo, bool isMiscellaneousFile, ILogger logger) + public async ValueTask<(OutputKind OutputKind, ImmutableArray MetadataReferences, bool NeedsRestore)> UpdateWithNewProjectInfoAsync(ProjectFileInfo newProjectInfo, bool isMiscellaneousFile, bool hasAllInformation, ILogger logger) { if (_mostRecentFileInfo != null) { @@ -158,7 +165,7 @@ public void Dispose() _projectSystemProject.GeneratedFilesOutputDirectory = newProjectInfo.GeneratedFilesOutputDirectory; _projectSystemProject.CompilationOutputAssemblyFilePath = newProjectInfo.IntermediateOutputFilePath; _projectSystemProject.DefaultNamespace = newProjectInfo.DefaultNamespace; - _projectSystemProject.HasAllInformation = !isMiscellaneousFile; + _projectSystemProject.HasAllInformation = hasAllInformation; if (newProjectInfo.TargetFrameworkIdentifier != null) { @@ -172,8 +179,21 @@ public void Dispose() newProjectInfo.Documents, _mostRecentFileInfo?.Documents, DocumentFileInfoComparer.Instance, - document => _projectSystemProject.AddSourceFile(document.FilePath, folders: document.Folders), - document => _projectSystemProject.RemoveSourceFile(document.FilePath), + document => + { + if (PathUtilities.IsAbsolute(document.FilePath)) + _projectSystemProject.AddSourceFile(document.FilePath, folders: document.Folders); + else + // When the file doesn't have an absolute path, then we think it doesn't exist on disk. + // e.g. it is a virtual document for an unsaved file or similar. + // In this case we just put a SourceTextContainer with empty text for it and rely on the LSP's solution forking to ensure it has up to date text. + _projectSystemProject.AddSourceTextContainer(SourceText.From("").Container, document.FilePath, folders: document.Folders); + }, + document => + { + Contract.ThrowIfFalse(PathUtilities.IsAbsolute(document.FilePath), "We do not expect to remove a file which is not on disk from the project."); + _projectSystemProject.RemoveSourceFile(document.FilePath); + }, "Project {0} now has {1} source file(s). ({2} added, {3} removed.)"); var relativePathResolver = new RelativePathResolver(commandLineArguments.ReferencePaths, commandLineArguments.BaseDirectory); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LspNavigateToSearchHostService.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LspNavigateToSearchHostService.cs new file mode 100644 index 000000000000..ac2b16663f18 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LspNavigateToSearchHostService.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.NavigateTo; + +namespace Microsoft.CodeAnalysis.LanguageServer.Features.NavigateTo; + +/// +/// LSP implementation of that always reports +/// as fully loaded. In LSP scenarios, we don't need the remote host hydration check since we +/// don't use the remote host for navigate-to operations. +/// +[ExportWorkspaceService(typeof(IWorkspaceNavigateToSearcherHostService), ServiceLayer.Host), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class LspNavigateToSearchHostService() : IWorkspaceNavigateToSearcherHostService +{ + public ValueTask IsFullyLoadedAsync(CancellationToken cancellationToken) + => new(true); +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/MetadataService.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/MetadataService.cs deleted file mode 100644 index d4a1b08604cd..000000000000 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/MetadataService.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Composition; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; - -[ExportWorkspaceServiceFactory(typeof(IMetadataService), ServiceLayer.Host), Shared] -internal sealed class MetadataServiceFactory : IWorkspaceServiceFactory -{ - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public MetadataServiceFactory() - { - } - - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - { - return new MetadataService(workspaceServices.GetRequiredService()); - } - - internal sealed class MetadataService : IMetadataService - { - private readonly MetadataReferenceCache _metadataCache; - - public MetadataService(IDocumentationProviderService documentationProviderService) - { - _metadataCache = new MetadataReferenceCache((path, properties) => - MetadataReference.CreateFromFile(path, properties, documentationProviderService.GetDocumentationProvider(path))); - } - - public PortableExecutableReference GetReference(string resolvedPath, MetadataReferenceProperties properties) - { - try - { - return (PortableExecutableReference)_metadataCache.GetReference(resolvedPath, properties); - } - catch (IOException ex) - { - return new ThrowingExecutableReference(resolvedPath, properties, ex); - } - } - - private sealed class ThrowingExecutableReference : PortableExecutableReference - { - private readonly IOException _ex; - - public ThrowingExecutableReference(string resolvedPath, MetadataReferenceProperties properties, IOException ex) : base(properties, resolvedPath) - { - _ex = ex; - } - - protected override DocumentationProvider CreateDocumentationProvider() - { - throw new NotImplementedException(); - } - - protected override Metadata GetMetadataImpl() - { - throw _ex; - } - - protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties) - { - return new ThrowingExecutableReference(FilePath!, properties, _ex); - } - } - } -} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectDependencyHelper.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectDependencyHelper.cs index 0311e437f410..c0cc8ea9ef65 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectDependencyHelper.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectDependencyHelper.cs @@ -68,7 +68,7 @@ private static bool CheckProjectAssetsForUnresolvedDependencies(ProjectFileInfo var lockFile = lockFileFormat.Read(projectAssetsPath); var projectAssetsMap = CreateProjectAssetsMap(lockFile); - using var _ = PooledHashSet.GetInstance(out var unresolved); + using var _ = PooledHashSet.GetInstance(out var unresolved); foreach (var reference in projectFileInfo.PackageReferences) { diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VSCodeSettings.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VSCodeSettings.cs new file mode 100644 index 000000000000..618f42f2d7f9 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/VSCodeSettings.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +internal sealed class VSCodeSettings(JsonElement rootElement) +{ + public static bool TryRead(string settingsFilePath, ILogger logger, [NotNullWhen(true)] out VSCodeSettings? settings) + { + if (File.Exists(settingsFilePath)) + { + try + { + var json = File.ReadAllText(settingsFilePath); + var options = new JsonDocumentOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip, + }; + + using var document = JsonDocument.Parse(json, options); + if (document.RootElement.ValueKind != JsonValueKind.Object) + { + settings = null; + return false; + } + + settings = new VSCodeSettings(document.RootElement.Clone()); + return true; + } + catch (Exception exception) + { + logger.LogWarning(exception, "Failed to parse VS Code settings file {SettingsFilePath}.", settingsFilePath); + } + } + + settings = null; + return false; + } + + public string? TryGetStringSetting(string propertyName) + { + if (!rootElement.TryGetProperty(propertyName, out var propertyValue) + || propertyValue.ValueKind != JsonValueKind.String) + { + return null; + } + + return propertyValue.GetString(); + } + + public static class Names + { + public const string DefaultSolution = "dotnet.defaultSolution"; + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs index 8e8cae416cea..9950971fe930 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs @@ -35,9 +35,8 @@ internal sealed class WorkspaceProjectFactoryService( Task IExportedBrokeredService.InitializeAsync(CancellationToken cancellationToken) => _projectInitializationHandler.SubscribeToInitializationCompleteAsync(cancellationToken); - public async Task CreateAndAddProjectAsync(WorkspaceProjectCreationInfo creationInfo, CancellationToken _) + public async Task CreateAndAddProjectAsync(WorkspaceProjectCreationInfo creationInfo, CancellationToken cancellationToken) { - _logger.LogInformation(string.Format(LanguageServerResources.Project_0_loaded_by_CSharp_Dev_Kit, creationInfo.FilePath)); VSCodeRequestTelemetryLogger.ReportProjectLoadStarted(); try { @@ -50,13 +49,20 @@ public async Task CreateAndAddProjectAsync(WorkspaceProjectCr creationInfo.DisplayName, creationInfo.Language, new Workspaces.ProjectSystem.ProjectSystemProjectCreationInfo { FilePath = creationInfo.FilePath }, - _workspaceFactory.ProjectSystemHostInfo); + _workspaceFactory.ProjectSystemHostInfo, + cancellationToken).ConfigureAwait(false); + + // We have now created the project and added it to the solution -- we are committed at this point + // to returning a project or else we would never have a way to remove this project we created. + cancellationToken = CancellationToken.None; var workspaceProject = new WorkspaceProject(project, _workspaceFactory.HostWorkspace.Services.SolutionServices, _workspaceFactory.TargetFrameworkManager, _loggerFactory); // We've created a new project, so initialize properties we have await workspaceProject.SetBuildSystemPropertiesAsync(creationInfo.BuildSystemProperties, CancellationToken.None); + _logger.LogInformation(string.Format(LanguageServerResources.Project_0_loaded_by_CSharp_Dev_Kit, creationInfo.FilePath)); + return workspaceProject; } catch (Exception e) when (LanguageServerFatalError.ReportAndLogAndPropagate(e, _logger, $"Failed to create project {creationInfo.DisplayName}")) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs index 0377b842a3f3..948aa78cb62e 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs @@ -63,7 +63,18 @@ public void Start() public async Task WaitForExitAsync() { - await _jsonRpc.Completion; + try + { + await _jsonRpc.Completion; + } + catch (Exception) + { + // The JsonRpc connection threw an exception. This usually means the client disconnected unexpectedly while + // the server was reading from it. We don't need this to cause the process to crash and trigger watsons, + // so we handle it and let the process exit. The server handles the JSON RPC disconnect event and will + // propagate unexpected errors through WaitForExitAsync. + } + await _roslynLanguageServer.WaitForExitAsync(); } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj index 0ab62d089743..d63392a41a7b 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj @@ -6,76 +6,33 @@ enable true - - true - - $(PackRuntimeIdentifier) - - - false - $(AssemblyName).$(PackRuntimeIdentifier) - - PackPublishContent;$(BeforePack) - - - $(NoWarn);NU5100;NETSDK1206 + + true + true + roslyn-language-server + roslyn-language-server + README.md false $(ArtifactsDir)/LanguageServer/$(Configuration)/$(TargetFramework)/$(RuntimeIdentifier) - $(ArtifactsDir)/LanguageServer/$(Configuration)/$(TargetFramework)/neutral - - $(TargetRid) - $(PortableTargetRid) - win-x64;win-arm64;linux-x64;linux-arm64;linux-musl-x64;linux-musl-arm64;osx-x64;osx-arm64 - - - false - false + win-x64;win-arm64;linux-x64;linux-arm64;linux-musl-x64;linux-musl-arm64;osx-x64;osx-arm64 - true + true - - - - PackRuntimeIdentifier - - + + + - - - @@ -110,7 +67,6 @@ - @@ -176,20 +132,32 @@ - - - - - true - content\LanguageServer\$(PackRuntimeIdentifier) - false - None - - - + + + + <_SymbolExclusionsFile>$(RepoRoot)eng\SymbolPublishingExclusionsFile.txt + <_ExpectedArm64Entry>tools/$(TargetFramework)/linux-musl-arm64/libe_sqlite3.so + <_ExpectedX64Entry>tools/$(TargetFramework)/linux-musl-x64/libe_sqlite3.so + + + + + - - + + <_ExclusionFileContent>@(_SymbolExclusionLines) + <_HasArm64Entry>$(_ExclusionFileContent.Contains('$(_ExpectedArm64Entry)')) + <_HasX64Entry>$(_ExclusionFileContent.Contains('$(_ExpectedX64Entry)')) + + + + + + diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/PackAllRids.targets b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/PackAllRids.targets deleted file mode 100644 index e4937d844a1e..000000000000 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/PackAllRids.targets +++ /dev/null @@ -1,41 +0,0 @@ - - - - false - <_RoslynPublishReadyToRun>false - <_RoslynPublishReadyToRun Condition="'$(Configuration)' == 'Release'">true - - - - - - - - - - - PackRuntimeIdentifier=%(RuntimeIdentifierForPack.Identity) - - - - - - - - - - - - diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs index 712b0497dde0..00218652078f 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs @@ -6,7 +6,7 @@ using System.Diagnostics; using System.IO.Pipes; using System.Runtime.InteropServices; -using System.Text.Json; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Contracts.Telemetry; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServer; @@ -15,16 +15,10 @@ using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.Logging; using Microsoft.CodeAnalysis.LanguageServer.Services; -using Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; -using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; using RoslynLog = Microsoft.CodeAnalysis.Internal.Log; -// Setting the title can fail if the process is run without a window, such -// as when launched detached from nodejs -IOUtilities.PerformIO(() => Console.Title = "Microsoft.CodeAnalysis.LanguageServer"); - WindowsErrorReporting.SetErrorModeOnWindows(); var command = CreateCommand(); @@ -39,14 +33,19 @@ static async Task RunAsync(ServerConfiguration serverConfiguration, CancellationToken cancellationToken) { - if (serverConfiguration.UseStdIo) + if (serverConfiguration.UseStdIo && serverConfiguration.ServerPipeName is not null) { - if (serverConfiguration.ServerPipeName is not null) - { - throw new InvalidOperationException("Server cannot be started with both --stdio and --pipe options."); - } + throw new InvalidOperationException("Server cannot be started with both --stdio and --pipe options."); + } - // Redirect Console.Out to try prevent the standard output stream from being corrupted. + if (!serverConfiguration.UseStdIo && serverConfiguration.ServerPipeName is null) + { + throw new InvalidOperationException("Server must be started with either --stdio or --pipe option."); + } + + if (serverConfiguration.UseStdIo) + { + // Redirect Console.Out to try prevent the standard output stream from being corrupted. // This should be done before the logger is created as it can write to the standard output. Console.SetOut(new StreamWriter(Console.OpenStandardError())); } @@ -100,13 +99,15 @@ static async Task RunAsync(ServerConfiguration serverConfiguration, Cancellation using var exportProvider = await LanguageServerExportProviderBuilder.CreateExportProviderAsync(AppContext.BaseDirectory, extensionManager, assemblyLoader, serverConfiguration.DevKitDependencyPath, cacheDirectory, loggerFactory, cancellationToken); - // LSP server doesn't have the pieces yet to support 'balanced' mode for source-generators. Hardcode us to - // 'automatic' for now. var globalOptionService = exportProvider.GetExportedValue(); - globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, SourceGeneratorExecutionPreference.Automatic); + globalOptionService.SetGlobalOption(WorkspaceConfigurationOptionsStorage.SourceGeneratorExecution, serverConfiguration.SourceGeneratorExecutionPreference); + logger.LogTrace("Source generator execution preference set to {preference}", serverConfiguration.SourceGeneratorExecutionPreference); // The log file directory passed to us by VSCode might not exist yet, though its parent directory is guaranteed to exist. - Directory.CreateDirectory(serverConfiguration.ExtensionLogDirectory); + if (serverConfiguration.ExtensionLogDirectory is not null) + { + Directory.CreateDirectory(serverConfiguration.ExtensionLogDirectory); + } // Initialize the server configuration MEF exported value. exportProvider.GetExportedValue().InitializeConfiguration(serverConfiguration); @@ -120,32 +121,23 @@ static async Task RunAsync(ServerConfiguration serverConfiguration, Cancellation var workspaceFactory = exportProvider.GetExportedValue(); var serviceBrokerFactory = exportProvider.GetExportedValue(); - StarredCompletionAssemblyHelper.InitializeInstance(serverConfiguration.StarredCompletionsPath, extensionManager, loggerFactory, serviceBrokerFactory); - LanguageServerHost? server = null; + LanguageServerHost server; if (serverConfiguration.UseStdIo) { server = new LanguageServerHost(Console.OpenStandardInput(), Console.OpenStandardOutput(), exportProvider, loggerFactory, typeRefResolver); } else { - var (clientPipeName, serverPipeName) = serverConfiguration.ServerPipeName is null - ? CreateNewPipeNames() - : (serverConfiguration.ServerPipeName, serverConfiguration.ServerPipeName); - - var pipeServer = new NamedPipeServerStream(serverPipeName, - PipeDirection.InOut, - maxNumberOfServerInstances: 1, - PipeTransmissionMode.Byte, - PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); - - // Send the named pipe connection info to the client - Console.WriteLine(JsonSerializer.Serialize(new NamedPipeInformation(clientPipeName))); - - // Wait for connection from client - await pipeServer.WaitForConnectionAsync(cancellationToken); - - server = new LanguageServerHost(pipeServer, pipeServer, exportProvider, loggerFactory, typeRefResolver); + // The VS Code LSP client passes a full pipe path (e.g. \\.\pipe\ on Windows, /tmp/.sock on Unix). + // NamedPipeClientStream expects just the pipe name on Windows (it prepends \\.\pipe\ itself), + // and the full socket path on Unix. + var pipeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? serverConfiguration.ServerPipeName!.Replace(@"\\.\pipe\", "") + : serverConfiguration.ServerPipeName!; + var pipeClient = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); + await pipeClient.ConnectAsync(cancellationToken); + server = new LanguageServerHost(pipeClient, pipeClient, exportProvider, loggerFactory, typeRefResolver); } server.Start(); @@ -155,6 +147,9 @@ static async Task RunAsync(ServerConfiguration serverConfiguration, Cancellation try { + if (serverConfiguration.ClientProcessId is int clientProcessId && RoslynLanguageServer.TryRegisterClientProcessId(clientProcessId)) + logger.LogInformation("Monitoring client process {clientProcessId} for exit", clientProcessId); + await server.WaitForExitAsync(); } finally @@ -181,14 +176,9 @@ static RootCommand CreateCommand() Required = false, }; - var logLevelOption = new Option("--logLevel") + var logLevelOption = new Option("--logLevel") { Description = "The minimum log verbosity.", - Required = true, - }; - var starredCompletionsPathOption = new Option("--starredCompletionComponentPath") - { - Description = "The location of the starred completion component (if one exists).", Required = false, }; @@ -197,10 +187,10 @@ static RootCommand CreateCommand() Description = "Telemetry level, Defaults to 'off'. Example values: 'all', 'crash', 'error', or 'off'.", Required = false, }; - var extensionLogDirectoryOption = new Option("--extensionLogDirectory") + var extensionLogDirectoryOption = new Option("--extensionLogDirectory") { Description = "The directory where we should write log files to", - Required = true, + Required = false, }; var sessionIdOption = new Option("--sessionId") @@ -250,7 +240,27 @@ static RootCommand CreateCommand() Description = "Use stdio for communication with the client.", Required = false, DefaultValueFactory = _ => false, + }; + + var autoLoadProjectsOption = new Option("--autoLoadProjects") + { + Description = "The server should automatically discover and load projects based on the workspace folders", + Required = false, + DefaultValueFactory = _ => false, + }; + var sourceGeneratorExecutionOption = new Option("--sourceGeneratorExecutionPreference") + { + Description = "Controls when source generators are executed.", + Required = false, + // Balanced mode requires additional client side support (to trigger refreshes), so by default run in automatic to ensure tool scenarios without client support run generators. + DefaultValueFactory = _ => SourceGeneratorExecutionPreference.Automatic, + }; + + var clientProcessIdOption = new Option("--clientProcessId") + { + Description = "The process ID of the client process. The server will terminate when the client process exits.", + Required = false, }; var rootCommand = new RootCommand() @@ -258,7 +268,6 @@ static RootCommand CreateCommand() debugOption, brokeredServicePipeNameOption, logLevelOption, - starredCompletionsPathOption, telemetryLevelOption, sessionIdOption, extensionAssemblyPathsOption, @@ -268,28 +277,32 @@ static RootCommand CreateCommand() csharpDesignTimePathOption, extensionLogDirectoryOption, serverPipeNameOption, - useStdIoOption + useStdIoOption, + autoLoadProjectsOption, + sourceGeneratorExecutionOption, + clientProcessIdOption, }; rootCommand.SetAction((parseResult, cancellationToken) => { var launchDebugger = parseResult.GetValue(debugOption); var logLevel = parseResult.GetValue(logLevelOption); - var starredCompletionsPath = parseResult.GetValue(starredCompletionsPathOption); var telemetryLevel = parseResult.GetValue(telemetryLevelOption); var sessionId = parseResult.GetValue(sessionIdOption); var extensionAssemblyPaths = parseResult.GetValue(extensionAssemblyPathsOption) ?? []; var devKitDependencyPath = parseResult.GetValue(devKitDependencyPathOption); var razorDesignTimePath = parseResult.GetValue(razorDesignTimePathOption); var csharpDesignTimePath = parseResult.GetValue(csharpDesignTimePathOption); - var extensionLogDirectory = parseResult.GetValue(extensionLogDirectoryOption)!; + var extensionLogDirectory = parseResult.GetValue(extensionLogDirectoryOption); var serverPipeName = parseResult.GetValue(serverPipeNameOption); var useStdIo = parseResult.GetValue(useStdIoOption); + var autoLoadProjects = parseResult.GetValue(autoLoadProjectsOption); + var sourceGeneratorExecutionPreference = parseResult.GetValue(sourceGeneratorExecutionOption); + var clientProcessId = parseResult.GetValue(clientProcessIdOption); var serverConfiguration = new ServerConfiguration( LaunchDebugger: launchDebugger, - LogConfiguration: new LogConfiguration(logLevel), - StarredCompletionsPath: starredCompletionsPath, + LogConfiguration: new LogConfiguration(logLevel ?? LogLevel.Information), TelemetryLevel: telemetryLevel, SessionId: sessionId, ExtensionAssemblyPaths: extensionAssemblyPaths, @@ -298,32 +311,13 @@ static RootCommand CreateCommand() CSharpDesignTimePath: csharpDesignTimePath, ServerPipeName: serverPipeName, UseStdIo: useStdIo, - ExtensionLogDirectory: extensionLogDirectory); + ExtensionLogDirectory: extensionLogDirectory, + AutoLoadProjects: autoLoadProjects, + SourceGeneratorExecutionPreference: sourceGeneratorExecutionPreference, + ClientProcessId: clientProcessId); return RunAsync(serverConfiguration, cancellationToken); }); return rootCommand; } - -static (string clientPipe, string serverPipe) CreateNewPipeNames() -{ - // On windows, .NET and Nodejs use different formats for the pipe name - const string WINDOWS_NODJS_PREFIX = @"\\.\pipe\"; - const string WINDOWS_DOTNET_PREFIX = @"\\.\"; - - // The pipe name constructed by some systems is very long (due to temp path). - // Shorten the unique id for the pipe. - var newGuid = Guid.NewGuid().ToString(); - var pipeName = newGuid.Split('-')[0]; - - return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? (WINDOWS_NODJS_PREFIX + pipeName, WINDOWS_DOTNET_PREFIX + pipeName) - : (GetUnixTypePipeName(pipeName), GetUnixTypePipeName(pipeName)); -} - -static string GetUnixTypePipeName(string pipeName) -{ - // Unix-type pipes are actually writing to a file - return Path.Combine(Path.GetTempPath(), pipeName + ".sock"); -} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/README.md b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/README.md new file mode 100644 index 000000000000..745722057c6b --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/README.md @@ -0,0 +1,65 @@ +# roslyn-language-server + +A [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/) implementation for C# powered by Roslyn. + +## Overview + +The `roslyn-language-server` is a .NET tool that provides rich language features for C# through the Language Server Protocol. It powers editor integrations including the [C# extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) and C# Dev Kit. + +This tool implements the LSP specification, enabling features such as: + +- IntelliSense (code completion) +- Go to definition +- Find all references +- Code fixes and refactorings +- Diagnostics and errors +- Hover information +- Document formatting +- And more + +## Installation + +Install the language server as a .NET tool: + +```bash +dotnet tool install --global roslyn-language-server --prerelease +``` + +## Usage + +The language server is designed to be launched by editor clients and typically should not be run directly by end users. It communicates via standard input/output or named pipes. + +### Command-line Options + +All options are optional. One of `--stdio` or `--pipe` should typically be specified for communication. + +> **Note:** Command-line options are subject to change in future versions. + +- `--stdio` - Use standard I/O for communication with the client (default: false) +- `--pipe ` - Use a named pipe for communication +- `--autoLoadProjects` - Automatically discover and load projects based on workspace folders (default: false) +- `--logLevel ` - Set the minimum log verbosity: Trace, Debug, Information, Warning, Error, or None (default: Information) +- `--extensionLogDirectory ` - Directory for log files +- `--extension ` - Load extension assemblies (can be specified multiple times) +- `--debug` - Launch the debugger on startup (default: false) +- `--telemetryLevel ` - Set telemetry level: all, crash, error, or off (default: off) +- And other specialized options for advanced scenarios + +### Example + +```bash +roslyn-language-server --stdio --autoLoadProjects +``` + +## Requirements + +- .NET 10.0 or later runtime + +## More Information + +- [Roslyn GitHub Repository](https://github.com/dotnet/roslyn) +- [Language Server Protocol Specification](https://microsoft.github.io/language-server-protocol/) + +## License + +This tool is part of the .NET Compiler Platform ("Roslyn") and is licensed under the [MIT license](https://github.com/dotnet/roslyn/blob/main/License.txt). diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ServerConfigurationFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ServerConfigurationFactory.cs index 586d85e8ae2d..3e7c5b5e54a1 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ServerConfigurationFactory.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ServerConfigurationFactory.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Composition; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; using Microsoft.Extensions.Logging; @@ -43,7 +44,6 @@ public void InitializeConfiguration(ServerConfiguration serverConfiguration) internal sealed record class ServerConfiguration( bool LaunchDebugger, LogConfiguration LogConfiguration, - string? StarredCompletionsPath, string? TelemetryLevel, string? SessionId, IEnumerable ExtensionAssemblyPaths, @@ -52,7 +52,10 @@ internal sealed record class ServerConfiguration( string? CSharpDesignTimePath, string? ServerPipeName, bool UseStdIo, - string ExtensionLogDirectory); + string? ExtensionLogDirectory, + bool AutoLoadProjects, + SourceGeneratorExecutionPreference SourceGeneratorExecutionPreference, + int? ClientProcessId); internal sealed class LogConfiguration { @@ -60,7 +63,7 @@ internal sealed class LogConfiguration public LogConfiguration(LogLevel initialLogLevel) { - _currentLogLevel = (int)initialLogLevel; + _currentLogLevel = (int)(initialLogLevel); } public void UpdateLogLevel(LogLevel level) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/ExtensionAssemblyManager.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/ExtensionAssemblyManager.cs index a646093208fe..5a7ebac1cb27 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/ExtensionAssemblyManager.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/ExtensionAssemblyManager.cs @@ -5,7 +5,6 @@ using System.Collections.Immutable; using System.Reflection; using System.Runtime.Loader; -using Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; using Microsoft.Extensions.Logging; @@ -58,18 +57,6 @@ public static ExtensionAssemblyManager Create(ServerConfiguration serverConfigur var assemblyFullNameToLoadContext = new Dictionary(StringComparer.Ordinal); using var _ = ArrayBuilder.GetInstance(out var validExtensionAssemblies); - if (serverConfiguration.StarredCompletionsPath is not null) - { - // HACK: Load the intellicode dll as an extension, but importantly do not add it to the valid extension assemblies set. - // While we do want to load it into its own ALC because it comes from a different ship vehicle, we do not want it - // to contribute to the MEF catalog / analyzers as a 'normal' extension would. Instead it gets reflection loaded elsewhere. - // - // We should migrate the intellicode completion provider to be a normal extension component with MEF provided parts, - // but it requires changes to the intellicode vscode extension and here to access our IServiceBroker instance via MEF. - var starredCompletionsComponentDll = StarredCompletionAssemblyHelper.GetStarredCompletionAssemblyPath(serverConfiguration.StarredCompletionsPath); - Contract.ThrowIfFalse(TryGetOrCreateLoadContext(starredCompletionsComponentDll)); - } - foreach (var assemblyFilePath in serverConfiguration.ExtensionAssemblyPaths) { if (TryGetOrCreateLoadContext(assemblyFilePath)) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionProvider.cs deleted file mode 100644 index 0706f9d30e2e..000000000000 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Composition; -using Microsoft.CodeAnalysis.Completion; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.LanguageService; - -namespace Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; - -[ExportCompletionProvider("CSharpStarredCompletionProvider", LanguageNames.CSharp), Shared] -internal sealed class StarredCompletionProvider : CompletionProvider -{ - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public StarredCompletionProvider() { } - - public override async Task ProvideCompletionsAsync(CompletionContext context) - { - var provider = await StarredCompletionAssemblyHelper.GetCompletionProviderAsync(context.CancellationToken); - if (provider == null) - { - return; //no-op if provider cannot be retrieved from assembly - } - await provider.ProvideCompletionsAsync(context); - } - - public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) - { - var provider = await StarredCompletionAssemblyHelper.GetCompletionProviderAsync(cancellationToken); - Contract.ThrowIfNull(provider, "ProvideCompletionsAsync must have completed successfully for GetChangeAsync to be called"); - return await provider.GetChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false); - } - - internal override async Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) - { - var provider = await StarredCompletionAssemblyHelper.GetCompletionProviderAsync(cancellationToken); - Contract.ThrowIfNull(provider, "ProvideCompletionsAsync must have completed successfully for GetDescriptionAsync to be called"); - return await provider.GetDescriptionAsync(document, item, options, displayOptions, cancellationToken); - } -} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionsAssemblyHelper.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionsAssemblyHelper.cs deleted file mode 100644 index 8c65c4b7e3b5..000000000000 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionsAssemblyHelper.cs +++ /dev/null @@ -1,149 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Reflection; -using Microsoft.CodeAnalysis.Completion; -using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; -using Microsoft.CodeAnalysis.LanguageServer.Services; -using Microsoft.Extensions.Logging; -using Microsoft.ServiceHub.Framework; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; - -internal static class StarredCompletionAssemblyHelper -{ - private const string CompletionsDllName = "Microsoft.VisualStudio.IntelliCode.CSharp.dll"; - private const string CompletionHelperClassFullName = "PythiaCSDevKit.CSDevKitCompletionHelper"; - private const string CreateCompletionProviderMethodName = "CreateCompletionProviderAsync"; - - // The following fields are only set as a part of the call to InitializeInstance, which is only called once for the lifetime of the process. Thus, it is safe to assume that once - // set, they will never change again. - private static string? s_completionsAssemblyLocation; - private static ILogger? s_logger; - private static ServiceBrokerFactory? s_serviceBrokerFactory; - private static ExtensionAssemblyManager? s_extensionAssemblyManager; - - /// - /// A gate to guard the actual creation of . This just prevents us from trying to create the provider more than once; once the field is set it - /// won't change again. - /// - private static readonly SemaphoreSlim s_gate = new(initialCount: 1); - private static bool s_previousCreationFailed = false; - private static CompletionProvider? s_completionProvider; - - /// - /// Initializes CompletionsAssemblyHelper singleton - /// - /// Location of dll for starred completion - /// Factory for creating new logger - /// Service broker with access to necessary remote services - internal static void InitializeInstance(string? completionsAssemblyLocation, ExtensionAssemblyManager extensionAssemblyManager, ILoggerFactory loggerFactory, ServiceBrokerFactory serviceBrokerFactory) - { - // No location provided means it wasn't passed through from C# Dev Kit, so we don't need to initialize anything further - if (string.IsNullOrEmpty(completionsAssemblyLocation)) - { - return; - } - - // C# Dev Kit must be installed, so we should be able to provide this; however we may not yet have a connection to the Dev Kit service broker, so we need to defer the actual creation - // until that point. - s_completionsAssemblyLocation = completionsAssemblyLocation; - s_logger = loggerFactory.CreateLogger(typeof(StarredCompletionAssemblyHelper)); - s_serviceBrokerFactory = serviceBrokerFactory; - s_extensionAssemblyManager = extensionAssemblyManager; - } - - internal static string GetStarredCompletionAssemblyPath(string starredCompletionComponentPath) - { - return Path.Combine(starredCompletionComponentPath, CompletionsDllName); - } - - internal static async Task GetCompletionProviderAsync(CancellationToken cancellationToken) - { - // Short cut: if we already have a provider, return it - if (s_completionProvider is CompletionProvider completionProvider) - return completionProvider; - - // If we don't have one because we previously failed to create one, then just return failure - if (s_previousCreationFailed) - return null; - - // If we were never initialized with any information from Dev Kit, we can't create one - if (s_completionsAssemblyLocation is null - || s_logger is null - || s_serviceBrokerFactory is null - || s_extensionAssemblyManager is null) - { - return null; - } - - // If we don't have a connection to a service broker yet, we also can't create one - var serviceBroker = s_serviceBrokerFactory.TryGetFullAccessServiceBroker(); - if (serviceBroker is null) - return null; - - // At this point, we have everything we need to go and create the provider, so let's do it - using (await s_gate.DisposableWaitAsync(cancellationToken)) - { - // Re-check this inside the lock, since we could have had a success between the earlier check and now - if (s_completionProvider is CompletionProvider completionProviderInsideLock) - return completionProviderInsideLock; - - // Re-check this inside the lock, since we could have had a failure between the earlier check and now - if (s_previousCreationFailed) - return null; - - try - { - var completionsDllPath = GetStarredCompletionAssemblyPath(s_completionsAssemblyLocation); - s_logger.LogTrace("trying to load intellicode provider"); - var starredCompletionsAssembly = s_extensionAssemblyManager.TryLoadAssemblyInExtensionContext(completionsDllPath); - if (starredCompletionsAssembly is null) - { - s_logger.LogTrace("failed to load intellicode provider"); - s_previousCreationFailed = true; - return null; - } - - var createCompletionProviderMethodInfo = GetMethodInfo(starredCompletionsAssembly, CompletionHelperClassFullName, CreateCompletionProviderMethodName); - - s_completionProvider = await CreateCompletionProviderAsync(createCompletionProviderMethodInfo, serviceBroker, s_completionsAssemblyLocation, s_logger); - return s_completionProvider; - } - catch (Exception ex) - { - s_previousCreationFailed = true; - s_logger.LogError(ex, "Unable to create the StarredCompletionProvider."); - throw; - } - } - } - - private static MethodInfo GetMethodInfo(Assembly assembly, string className, string methodName) - { - var completionHelperType = assembly.GetType(className); - if (completionHelperType == null) - { - throw new ArgumentException($"{assembly.FullName} assembly did not contain {className} class"); - } - var createCompletionProviderMethodInto = completionHelperType?.GetMethod(methodName); - if (createCompletionProviderMethodInto == null) - { - throw new ArgumentException($"{className} from {assembly.FullName} assembly did not contain {methodName} method"); - } - return createCompletionProviderMethodInto; - } - - private static async Task CreateCompletionProviderAsync(MethodInfo createCompletionProviderMethodInfo, IServiceBroker serviceBroker, string modelBasePath, ILogger logger) - { - var completionProviderObj = createCompletionProviderMethodInfo.Invoke(null, [serviceBroker, BrokeredServices.Services.Descriptors.RemoteModelService, modelBasePath, logger]); - if (completionProviderObj == null) - { - throw new NotSupportedException($"{createCompletionProviderMethodInfo.Name} method could not be invoked"); - } - var completionProvider = (Task)completionProviderObj; - return await completionProvider; - } -} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Testing/RunTestsHandler.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Testing/RunTestsHandler.cs index 404f3e4b0fe5..25491511d383 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Testing/RunTestsHandler.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Testing/RunTestsHandler.cs @@ -51,10 +51,11 @@ public async Task HandleRequestAsync(RunTestsParams req var dotnetRootUser = Environment.GetEnvironmentVariable("DOTNET_ROOT_USER"); + var testLogPath = serverConfiguration.ExtensionLogDirectory is not null ? Path.Combine(serverConfiguration.ExtensionLogDirectory, "testLogs", "vsTestLogs.txt") : null; // Instantiate the test platform wrapper. var vsTestConsoleWrapper = new VsTestConsoleWrapper(vsTestConsolePath, new ConsoleParameters { - LogFilePath = Path.Combine(serverConfiguration.ExtensionLogDirectory, "testLogs", "vsTestLogs.txt"), + LogFilePath = testLogPath, TraceLevel = GetTraceLevel(serverConfiguration), EnvironmentVariables = new() { diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleRequestContextFactory.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleRequestContextFactory.cs index 606762c26aa8..7d6c15f6e5ef 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleRequestContextFactory.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleRequestContextFactory.cs @@ -16,7 +16,7 @@ public ExampleRequestContextFactory(ILspServices lspServices) _lspServices = lspServices; } - public override async Task CreateRequestContextAsync(IQueueItem queueItem, IMethodHandler methodHandler, TRequestParam requestParam, CancellationToken cancellationToken) + public override async Task CreateRequestContextAsync(QueueItem queueItem, IMethodHandler methodHandler, TRequestParam requestParam, CancellationToken cancellationToken) { var logger = _lspServices.GetRequiredService(); diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/Microsoft.CommonLanguageServerProtocol.Framework.Example.csproj b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/Microsoft.CommonLanguageServerProtocol.Framework.Example.csproj index 6b419a03f3fc..8695faa3ebe2 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/Microsoft.CommonLanguageServerProtocol.Framework.Example.csproj +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/Microsoft.CommonLanguageServerProtocol.Framework.Example.csproj @@ -3,7 +3,7 @@ Library - $(NetVS);netstandard2.0 + $(NetRoslynAll);netstandard2.0 false false An example implementation of the Common Language Server Protocol Framework. diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests.csproj b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests.csproj index 0ede2bd0801e..582e79ebbb12 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests.csproj +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests.csproj @@ -1,4 +1,4 @@ - + diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/Mocks/TestRequestContext.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/Mocks/TestRequestContext.cs index 229fc9543cd9..1b93d0654dd8 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/Mocks/TestRequestContext.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/Mocks/TestRequestContext.cs @@ -13,7 +13,7 @@ internal sealed class Factory : AbstractRequestContextFactory CreateRequestContextAsync(IQueueItem queueItem, IMethodHandler methodHandler, TRequestParam requestParam, CancellationToken cancellationToken) + public override async Task CreateRequestContextAsync(QueueItem queueItem, IMethodHandler methodHandler, TRequestParam requestParam, CancellationToken cancellationToken) => new TestRequestContext(); } } diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/QueueItemTests.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/QueueItemTests.cs index fe499bfd3450..d5886510ab93 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/QueueItemTests.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/QueueItemTests.cs @@ -41,7 +41,6 @@ public async Task QueueItem_CancellationToken_Cancelled() request: null, context: 1, ThrowLocalRpcExceptionMethodHandler.Instance, - string.Empty, CancellationToken.None); }); Assert.Equal(LspErrorCodes.ContentModified, exception.ErrorCode); diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/TestExampleLanguageServer.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/TestExampleLanguageServer.cs index a09f06b3dcd1..443c6fc80f44 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/TestExampleLanguageServer.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/TestExampleLanguageServer.cs @@ -65,7 +65,7 @@ public async Task ExitAsync() _exitingSource.SetResult(0); } - public async Task ShutdownAsync(string message = "Shutting down") + public async Task ShutdownAsync() { _shuttingDownSource.SetResult(0); } diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs index 629f001c9468..bbf2024088c8 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Reflection; using System.Threading; @@ -229,7 +230,7 @@ public Task WaitForExitAsync() // Ensure we've actually been asked to shutdown before waiting. if (_shutdownRequestTask == null) { - throw new InvalidOperationException("The language server has not yet been asked to shutdown."); + throw new ServerNotShutDownException("The language server has not yet been asked to shutdown."); } } @@ -264,7 +265,7 @@ async Task Shutdown_NoLockAsync(string message) // Allow implementations to do any additional cleanup on shutdown. var lifeCycleManager = GetLspServices().GetRequiredService(); - await lifeCycleManager.ShutdownAsync(message).ConfigureAwait(false); + await lifeCycleManager.ShutdownAsync().ConfigureAwait(false); await ShutdownRequestExecutionQueueAsync().ConfigureAwait(false); } @@ -274,14 +275,16 @@ async Task Shutdown_NoLockAsync(string message) /// Tells the LSP server to exit. Requires that was called first. /// Typically called from an LSP exit notification. /// - public Task ExitAsync() + /// Optional exception that caused the server to shutdown. + /// When provided, will throw this exception so callers can observe the error. + public Task ExitAsync(Exception? shutdownException = null) { Task exitTask; lock (_lifeCycleLock) { if (_shutdownRequestTask?.IsCompleted != true) { - throw new InvalidOperationException("The language server has not yet been asked to shutdown or has not finished shutting down."); + throw new ServerNotShutDownException("The language server has not yet been asked to shutdown or has not finished shutting down."); } // Run exit or return the already running exit request. @@ -318,8 +321,14 @@ async Task Exit_NoLockAsync() } finally { - Logger.LogInformation("Exiting server"); - _serverExitedSource.TrySetResult(null); + if (shutdownException is not null) + { + _serverExitedSource.TrySetException(shutdownException); + } + else + { + _serverExitedSource.TrySetResult(null); + } } } } @@ -339,10 +348,34 @@ private void JsonRpc_Disconnected(object? sender, JsonRpcDisconnectedEventArgs e async Task JsonRpc_DisconnectedAsync(object? sender, JsonRpcDisconnectedEventArgs e) { + var exceptionToReport = TryGetReportableException(e); + // It is possible this gets called during normal shutdown and exit. // ShutdownAsync and ExitAsync will no-op if shutdown was already triggered by something else. - await ShutdownAsync(message: "Shutdown triggered by JsonRpc disconnect").ConfigureAwait(false); - await ExitAsync().ConfigureAwait(false); + await ShutdownAsync(message: $"Shutdown triggered by JsonRpc disconnect {e.Reason}").ConfigureAwait(false); + await ExitAsync(exceptionToReport).ConfigureAwait(false); + } + + Exception? TryGetReportableException(JsonRpcDisconnectedEventArgs e) + { + if (e.Exception == null) + { + return null; + } + + if (e.Reason == DisconnectedReason.RemotePartyTerminated || e.Reason == DisconnectedReason.LocallyDisposed) + { + // These are expected disconnect reasons that can occur during normal shutdown or if the client disconnects. + return null; + } + + if (e.Exception is IOException) + { + // Server communication is done over named pipes, IO exceptions are normal if the client disconnects unexpectedly while the server is in the middle of reading or writing. + return null; + } + + return e.Exception; } } diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs index 84849c484dc7..646537b13c79 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs @@ -12,10 +12,10 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// /// -/// A factory for creating objects from 's. +/// A factory for creating objects from 's. /// /// -/// RequestContext's are useful for passing document context, since by default +/// RequestContext's are useful for passing document context, since by default /// is run on the queue thread (and thus no mutating requests may be executing simultaneously, preventing possible race conditions). /// It also allows somewhere to pass things like the or which are useful on a wide variety of requests. /// @@ -24,14 +24,14 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; internal abstract class AbstractRequestContextFactory { /// - /// Create a object from the given . + /// Create a object from the given . /// Note - throwing in the implementation of this method will cause the server to shutdown. /// - /// The from which to create the request context. + /// The from which to create the request context. /// The for which to create the request context /// The request parameters. /// /// The for this request. /// This method is called on the queue thread to allow context to be retrieved serially, without the possibility of race conditions from Mutating requests. - public abstract Task CreateRequestContextAsync(IQueueItem queueItem, IMethodHandler methodHandler, TRequestParam requestParam, CancellationToken cancellationToken); + public abstract Task CreateRequestContextAsync(QueueItem queueItem, IMethodHandler methodHandler, TRequestParam requestParam, CancellationToken cancellationToken); } diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs index b8f704c68d15..383c442a703e 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs @@ -19,7 +19,7 @@ internal interface ILifeCycleManager /// Called when the server recieves the LSP exit notification. /// /// - /// This is always called after the LSP shutdown request and runs + /// This is always called after the LSP shutdown request and runs /// but before LSP services and the JsonRpc connection is disposed of in LSP exit. /// Implementations are not expected to be threadsafe. /// @@ -32,5 +32,5 @@ internal interface ILifeCycleManager /// This is called before the request execution is closed. /// Implementations are not expected to be threadsafe. /// - Task ShutdownAsync(string message = "Shutting down"); + Task ShutdownAsync(); } diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs deleted file mode 100644 index cc8135a6ef0e..000000000000 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Threading; -using System.Threading.Tasks; - -// This is consumed as 'generated' code in a source package and therefore requires an explicit nullable enable -#nullable enable - -namespace Microsoft.CommonLanguageServerProtocol.Framework; - -/// -/// An item to be queued for execution. -/// -/// The type of the request context to be passed along to the handler. -internal interface IQueueItem -{ - /// - /// Executes the work specified by this queue item. - /// - /// The request parameters. - /// The context created by . - /// The handler to use to execute the request. - /// The language for the request. - /// - /// A which completes when the request has finished. - Task StartRequestAsync(TRequest request, TRequestContext? context, IMethodHandler handler, string language, CancellationToken cancellationToken); - - /// - /// Creates the context that is sent to the handler for this queue item. - /// Note - this method is always called serially inside the queue before - /// running the actual request in - /// Throwing in this method will cause the server to shutdown. - /// - /// If there was a recoverable failure in creating the request, this will return null and the caller should stop processing the request. - /// - Task<(TRequestContext, TRequest)?> CreateRequestContextAsync(IMethodHandler handler, RequestHandlerMetadata requestHandlerMetadata, AbstractLanguageServer languageServer, CancellationToken cancellationToken); - - /// - /// Handles when the queue needs to manually fail a request before the - /// handler is invoked without shutting down the entire queue. - /// - void FailRequest(string message); - - /// - /// Provides access to LSP services. - /// - ILspServices LspServices { get; } - - /// - /// The method being executed. - /// - string MethodName { get; } - - object? SerializedRequest { get; } -} diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LspErrorCodes.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LspErrorCodes.cs index 2f0d90664043..38ad7df76d1c 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LspErrorCodes.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LspErrorCodes.cs @@ -14,4 +14,28 @@ internal static class LspErrorCodes /// outside of normal conditions. /// public const int ContentModified = -32801; + + /// + /// This is the end range of LSP reserved error codes. + /// It doesn't denote a real error code. + /// + public const int LspReservedErrorRangeEnd = -32800; + +} + +/// +/// Error codes used by the Roslyn LSP, but not standardized in general LSP. +/// +internal static class RoslynLspErrorCodes +{ + /// + /// Signals that the server could not process the request, but that the failure shouldn't be surfaced to the user. + /// (It's expected that the failure is still logged, however.) + /// + /// + /// This is only meant to be used under conditions where we can't fulfill the request, but we think that the failure + /// is unlikely to be significant to the user (i.e. surface as an actual editor feature failing to function properly.) + /// For example, if pull diagnostics are requested for a virtual document that was already closed. + /// + public const int NonFatalRequestFailure = -30099; } diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs index 82e8dbb77a92..18b4bb06f7d5 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs @@ -22,7 +22,7 @@ internal sealed class NoValue public static NoValue Instance = new(); } -internal sealed class QueueItem : IQueueItem +internal sealed class QueueItem { private readonly ILspLogger _logger; private readonly AbstractRequestScope? _requestTelemetryScope; @@ -67,7 +67,7 @@ internal QueueItem( _requestTelemetryScope = telemetryService?.CreateRequestScope(methodName); } - public static (IQueueItem, Task) Create( + public static (QueueItem, Task) Create( string methodName, object? serializedRequest, ILspServices lspServices, @@ -159,7 +159,7 @@ private bool TryDeserializeRequest( /// representing the task that the client is waiting for, then re-thrown so that /// the queue can correctly handle them depending on the type of request. /// - public async Task StartRequestAsync(TRequest request, TRequestContext? context, IMethodHandler handler, string language, CancellationToken cancellationToken) + public async Task StartRequestAsync(TRequest request, TRequestContext? context, IMethodHandler handler, CancellationToken cancellationToken) { _requestHandlingStarted = true; @@ -266,7 +266,7 @@ public void FailRequest(string message) { throw new InvalidOperationException("Cannot manually fail queue item after it has started"); } - var exception = new Exception(message); + var exception = new LocalRpcException(message) { ErrorCode = RoslynLspErrorCodes.NonFatalRequestFailure }; _requestTelemetryScope?.RecordException(exception); _logger.LogException(exception); diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs index 61b42df61ac6..c1b0155d3a09 100644 --- a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs @@ -65,7 +65,7 @@ internal class RequestExecutionQueue : IRequestExecutionQueue - protected readonly AsyncQueue<(IQueueItem queueItem, Guid ActivityId, CancellationToken cancellationToken)> _queue = new(); + protected readonly AsyncQueue<(QueueItem queueItem, Guid ActivityId, CancellationToken cancellationToken)> _queue = new(); private readonly CancellationTokenSource _cancelSource = new(); /// @@ -198,7 +198,7 @@ private async Task ProcessQueueAsync() { // First attempt to de-queue the work item in its own try-catch. // This is because before we de-queue we do not have access to the queue item's linked cancellation token. - (IQueueItem work, Guid activityId, CancellationToken cancellationToken) queueItem; + (QueueItem work, Guid activityId, CancellationToken cancellationToken) queueItem; try { queueItem = await _queue.DequeueAsync(_cancelSource.Token).ConfigureAwait(false); @@ -250,12 +250,26 @@ private async Task ProcessQueueAsync() using var languageScope = _logger.CreateLanguageContext(language); - // Now that we know the actual language, we can deserialize the request and start creating the request context. - var (metadata, handler, methodInfo) = GetHandlerForRequest(work, language ?? LanguageServerConstants.DefaultLanguageName); + // Now that we know the actual language, we can try to find the appropriate handler. + var resolvedLanguage = language ?? LanguageServerConstants.DefaultLanguageName; + if (!TryGetHandlerForRequest(work, resolvedLanguage, out var handlerResult)) + { + // No handler found for this method+language combination. This can happen if: + // 1. We were unable to determine the language and there is no default handler for this method. + // 2. A client sends a request for a method the server does not handle for this language. + // In either case, we should not crash - just fail the request gracefully. + work.FailRequest($"Missing handler for {work.MethodName} and language {resolvedLanguage}"); + continue; + } + + var (metadata, handler, methodInfo) = handlerResult; // We had an issue determining the language. Generally this is very rare and only occurs - // if there is a mis-behaving client that sends us requests for files where we haven't saved the languageId. - // We should only crash if this was a mutating method, otherwise we should just fail the single request. + // when a client sends us requests for files where we haven't saved the languageId. + // We should only crash if this was a mutating method. + // + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didClose + // > Note that a server’s ability to fulfill requests is independent of whether a text document is open or closed. if (!didGetLanguage) { var message = $"Failed to get language for {work.MethodName}"; @@ -266,7 +280,7 @@ private async Task ProcessQueueAsync() else { work.FailRequest(message); - return; + continue; } } @@ -291,7 +305,7 @@ private async Task ProcessQueueAsync() if (lspServices is not null) { await _languageServer.ShutdownAsync(message).ConfigureAwait(false); - await _languageServer.ExitAsync().ConfigureAwait(false); + await _languageServer.ExitAsync(ex).ConfigureAwait(false); } await DisposeAsync().ConfigureAwait(false); @@ -300,11 +314,11 @@ private async Task ProcessQueueAsync() } /// - /// Reflection invokes + /// Reflection invokes /// using the concrete types defined by the handler's metadata. /// private async Task InvokeProcessCoreAsync( - IQueueItem work, + QueueItem work, RequestHandlerMetadata metadata, IMethodHandler handler, MethodInfo methodInfo, @@ -327,7 +341,7 @@ private async Task InvokeProcessCoreAsync( /// waiting or not waiting on results as defined by the handler. /// private async Task ProcessQueueCoreAsync( - IQueueItem work, + QueueItem work, IMethodHandler handler, RequestHandlerMetadata metadata, ConcurrentDictionary concurrentlyExecutingTasks, @@ -366,7 +380,7 @@ private async Task ProcessQueueCoreAsync( Debug.Assert(!concurrentlyExecutingTasks.Any(t => !t.Key.IsCompleted), "The tasks should have all been drained before continuing"); // Mutating requests block other requests from starting to ensure an up to date snapshot is used. // Since we're explicitly awaiting exceptions to mutating requests will bubble up here. - await WrapStartRequestTaskAsync(work.StartRequestAsync(deserializedRequest, context, handler, metadata.Language, cancellationToken), rethrowExceptions: true).ConfigureAwait(false); + await WrapStartRequestTaskAsync(work.StartRequestAsync(deserializedRequest, context, handler, cancellationToken), rethrowExceptions: true).ConfigureAwait(false); } else { @@ -375,7 +389,7 @@ private async Task ProcessQueueCoreAsync( // though these errors don't put us into a bad state as far as the rest of the queue goes. // Furthermore we use Task.Run here to protect ourselves against synchronous execution of work // blocking the request queue for longer periods of time (it enforces parallelizability). - var currentWorkTask = WrapStartRequestTaskAsync(Task.Run(() => work.StartRequestAsync(deserializedRequest, context, handler, metadata.Language, cancellationToken), cancellationToken), rethrowExceptions: false); + var currentWorkTask = WrapStartRequestTaskAsync(Task.Run(() => work.StartRequestAsync(deserializedRequest, context, handler, cancellationToken), cancellationToken), rethrowExceptions: false); if (CancelInProgressWorkUponMutatingRequest) { @@ -410,16 +424,18 @@ protected internal virtual void BeforeRequest(TRequest request) return; } - private (RequestHandlerMetadata Metadata, IMethodHandler Handler, MethodInfo MethodInfo) GetHandlerForRequest(IQueueItem work, string language) + private bool TryGetHandlerForRequest(QueueItem work, string language, out (RequestHandlerMetadata Metadata, IMethodHandler Handler, MethodInfo MethodInfo) result) { var handlersForMethod = _handlerInfoMap[work.MethodName]; if (handlersForMethod.TryGetValue(language, out var lazyData) || handlersForMethod.TryGetValue(LanguageServerConstants.DefaultLanguageName, out lazyData)) { - return lazyData.Value; + result = lazyData.Value; + return true; } - throw new InvalidOperationException($"Missing default or language handler for {work.MethodName} and language {language}"); + result = default; + return false; } /// diff --git a/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ServerNotShutDownException.cs b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ServerNotShutDownException.cs new file mode 100644 index 000000000000..2deb5c8e7bdd --- /dev/null +++ b/src/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ServerNotShutDownException.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// This is consumed as 'generated' code in a source package and therefore requires an explicit nullable enable +#nullable enable + +using System; + +namespace Microsoft.CommonLanguageServerProtocol.Framework; + +/// +/// Thrown when an operation requires the language server to have been asked to shut down, +/// but shutdown has not yet been initiated or completed. +/// +internal sealed class ServerNotShutDownException : InvalidOperationException +{ + public ServerNotShutDownException(string message) + : base(message) + { + } +} diff --git a/src/LanguageServer/Protocol.TestUtilities/AbstractLspMiscellaneousFilesWorkspaceTests.cs b/src/LanguageServer/Protocol.TestUtilities/AbstractLspMiscellaneousFilesWorkspaceTests.cs index b54fed225311..3613bb9c4680 100644 --- a/src/LanguageServer/Protocol.TestUtilities/AbstractLspMiscellaneousFilesWorkspaceTests.cs +++ b/src/LanguageServer/Protocol.TestUtilities/AbstractLspMiscellaneousFilesWorkspaceTests.cs @@ -37,7 +37,7 @@ public async Task TestLooseFile_Opened(bool mutatingLspWorkspace) Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. - var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); + var looseFileUri = CreateAbsoluteDocumentUri("SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUri, """ class A { @@ -59,7 +59,7 @@ public async Task TestLooseFile_Changed(bool mutatingLspWorkspace) Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); - var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); + var looseFileUri = CreateAbsoluteDocumentUri("SomeFile.cs"); // Open an empty loose file and make a request to verify it gets added to the misc workspace. await testLspServer.OpenDocumentAsync(looseFileUri, string.Empty).ConfigureAwait(false); @@ -96,7 +96,7 @@ public async Task TestLooseFile_Closed(bool mutatingLspWorkspace) Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. - var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); + var looseFileUri = CreateAbsoluteDocumentUri("SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUri, """ class A { @@ -130,7 +130,7 @@ void M() Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open a file that is part of a registered workspace and verify it is not present in the misc workspace. - var document = await AddDocumentAsync(testLspServer, "C:\\SomeFile.cs", markup).ConfigureAwait(false); + var document = await AddDocumentAsync(testLspServer, TestHelpers.CreateAbsolutePath("SomeFile.cs"), markup).ConfigureAwait(false); var fileInWorkspaceUri = document.GetURI(); await testLspServer.OpenDocumentAsync(fileInWorkspaceUri, markup).ConfigureAwait(false); Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); @@ -155,7 +155,7 @@ void M() // Open an empty loose file and make a request to verify it gets added to the misc workspace. // Include some Unicode characters to test URL handling. - var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri("C:\\\ue25b\ud86d\udeac.cs"); + var looseFileUri = CreateAbsoluteDocumentUri("\ue25b\ud86d\udeac.cs"); var looseFileTextDocumentIdentifier = new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }; await testLspServer.OpenDocumentAsync(looseFileUri, source).ConfigureAwait(false); @@ -191,7 +191,7 @@ public async Task TestLooseFile_RazorFile(bool mutatingLspWorkspace) Assert.Null(await GetMiscellaneousAdditionalDocumentAsync(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. - var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.razor"); + var looseFileUri = CreateAbsoluteDocumentUri("SomeFile.razor"); await testLspServer.OpenDocumentAsync(looseFileUri, "
").ConfigureAwait(false); // Trigger a request and assert we got a file in the misc workspace. @@ -216,7 +216,7 @@ public async Task TestLooseFile_RequestedTwiceAndClosed(bool mutatingLspWorkspac Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. - var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); + var looseFileUri = CreateAbsoluteDocumentUri("SomeFile.cs"); await testLspServer.OpenDocumentAsync(looseFileUri, """ class A { @@ -391,7 +391,7 @@ public async Task TestLspTransfersFromMiscellaneousFilesToHostWorkspaceAsync(boo return documents.SingleOrDefault(); } - private static async ValueTask GetMiscellaneousAdditionalDocumentAsync(TestLspServer testLspServer) + private protected static async ValueTask GetMiscellaneousAdditionalDocumentAsync(TestLspServer testLspServer) { var documents = await testLspServer.GetManagerAccessor().GetMiscellaneousDocumentsAsync(static p => p.AdditionalDocuments).ToImmutableArrayAsync(CancellationToken.None); return documents.SingleOrDefault(); diff --git a/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.InitializationOptions.cs b/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.InitializationOptions.cs index b8d44c79656c..7dd5f5d5eabe 100644 --- a/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.InitializationOptions.cs +++ b/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.InitializationOptions.cs @@ -31,5 +31,6 @@ internal readonly record struct InitializationOptions() internal IEnumerable? AdditionalAnalyzers { get; init; } = null; internal IJsonRpcMessageFormatter? ClientMessageFormatter { get; init; } = null; internal ParseOptions? ParseOptions { get; init; } = null; + internal LSP.WorkspaceFolder[]? WorkspaceFolders { get; init; } = null; } } diff --git a/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs b/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs index 0af6527898a5..6f4e14c157b0 100644 --- a/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs +++ b/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs @@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; @@ -44,6 +45,9 @@ public abstract partial class AbstractLanguageServerProtocolTests { protected static readonly JsonSerializerOptions JsonSerializerOptions = RoslynLanguageServer.CreateJsonMessageFormatter().JsonSerializerOptions; + private protected static DocumentUri CreateAbsoluteDocumentUri(string suffix) + => ProtocolConversions.CreateAbsoluteDocumentUri(TestHelpers.CreateAbsolutePath(suffix)); + private protected readonly AbstractLspLogger TestOutputLspLogger; protected AbstractLanguageServerProtocolTests(ITestOutputHelper? testOutputHelper) { @@ -663,6 +667,7 @@ internal async Task InitializeAsync() { Capabilities = _initializationOptions.ClientCapabilities, Locale = _initializationOptions.Locale, + WorkspaceFolders = _initializationOptions.WorkspaceFolders, }, CancellationToken.None); } @@ -825,6 +830,15 @@ public Task CloseDocumentAsync(DocumentUri documentUri) return ExecuteRequestAsync(LSP.Methods.TextDocumentDidCloseName, didCloseParams, CancellationToken.None); } + public async Task RefreshSourceGeneratorsAsync(bool forceRegeneration) + { + var refreshSourceGeneratorsParams = new RefreshSourceGeneratorsParams(forceRegeneration); + + // The refresh command should trigger source generators to run in both automatic and balanced mode. + await this.ExecuteRequestAsync(WorkspaceRefreshSourceGeneratorsHandler.MethodName, refreshSourceGeneratorsParams, CancellationToken.None); + await this.WaitForSourceGeneratorsAsync(); + } + public async Task ShutdownTestServerAsync() { await _clientRpc.InvokeAsync(LSP.Methods.ShutdownName).ConfigureAwait(false); diff --git a/src/LanguageServer/Protocol.TestUtilities/Microsoft.CodeAnalysis.LanguageServer.Protocol.Test.Utilities.csproj b/src/LanguageServer/Protocol.TestUtilities/Microsoft.CodeAnalysis.LanguageServer.Protocol.Test.Utilities.csproj index aa9aef09e9e2..ab66ad96800d 100644 --- a/src/LanguageServer/Protocol.TestUtilities/Microsoft.CodeAnalysis.LanguageServer.Protocol.Test.Utilities.csproj +++ b/src/LanguageServer/Protocol.TestUtilities/Microsoft.CodeAnalysis.LanguageServer.Protocol.Test.Utilities.csproj @@ -20,7 +20,6 @@ - \ No newline at end of file diff --git a/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs b/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs index 024d37f65a47..e1411b94d4d0 100644 --- a/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs +++ b/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs @@ -20,7 +20,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer; /// -/// Implementation of that provides all the capabilities that Roslyn supports via LSP. +/// Implementation of that provides all the capabilities that Roslyn supports via LSP. /// [Export(typeof(DefaultCapabilitiesProvider)), Shared] [ExportCSharpVisualBasicStatelessLspService(typeof(ICapabilitiesProvider), WellKnownLspServerKinds.Any)] @@ -84,11 +84,17 @@ public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) }; capabilities.FoldingRangeProvider = true; + capabilities.SelectionRangeProvider = true; + capabilities.CallHierarchyProvider = true; capabilities.ExecuteCommandProvider = new ExecuteCommandOptions() { Commands = [] }; capabilities.TextDocumentSync = new TextDocumentSyncOptions { Change = TextDocumentSyncKind.Incremental, - OpenClose = true + OpenClose = true, + Save = new SaveOptions + { + IncludeText = false, + } }; capabilities.HoverProvider = true; diff --git a/src/LanguageServer/Protocol/Features/CodeCleanup/AbstractCodeCleanupService.cs b/src/LanguageServer/Protocol/Features/CodeCleanup/AbstractCodeCleanupService.cs index bdf40f1d0e9c..20959a3c9f92 100644 --- a/src/LanguageServer/Protocol/Features/CodeCleanup/AbstractCodeCleanupService.cs +++ b/src/LanguageServer/Protocol/Features/CodeCleanup/AbstractCodeCleanupService.cs @@ -24,6 +24,18 @@ internal abstract class AbstractCodeCleanupService(ICodeFixService codeFixServic { private readonly ICodeFixService _codeFixService = codeFixService; + /// + /// Diagnostic IDs whose code fixes generate stub implementations and should not be applied automatically during code cleanup. + /// Applying these fixes silently converts compile-time errors into runtime errors. + /// + private static readonly ImmutableHashSet s_implementMemberDiagnosticIds = ImmutableHashSet.Create( + "CS0535", // 'Type' does not implement interface member 'Member' + "CS0737", // 'Type' does not implement interface member 'Member' (not public) + "CS0738", // 'Type' does not implement interface member 'Member' (wrong return type) + "CS0534", // 'Type' does not implement inherited abstract member 'Member' + "BC30149", // Class 'Type' must implement 'Method' for interface 'Interface' + "BC30610"); // Class must either be declared 'MustInherit' or override inherited 'MustOverride' member(s) + protected abstract string OrganizeImportsDescription { get; } protected abstract ImmutableArray GetDiagnosticSets(); @@ -209,8 +221,8 @@ private async Task ApplyCodeFixesForSpecificDiagnosticIdAsync( var diagnosticService = document.Project.Solution.Services.GetRequiredService(); var diagnostics = await diagnosticService.GetDiagnosticsForSpanAsync( document, range, - // Compute diagnostics for everything that is *NOT* an IDE analyzer - DiagnosticIdFilter.Exclude(IDEDiagnosticIdToOptionMappingHelper.KnownIDEDiagnosticIds), + // Compute diagnostics for everything that is *NOT* an IDE analyzer and not an implement-member diagnostic. + DiagnosticIdFilter.Exclude(IDEDiagnosticIdToOptionMappingHelper.KnownIDEDiagnosticIds.Union(s_implementMemberDiagnosticIds)), priority: null, DiagnosticKind.All, cancellationToken).ConfigureAwait(false); diff --git a/src/LanguageServer/Protocol/Features/Options/FileBasedAppsOptionsStorage.cs b/src/LanguageServer/Protocol/Features/Options/FileBasedAppsOptionsStorage.cs new file mode 100644 index 000000000000..19863917b6e2 --- /dev/null +++ b/src/LanguageServer/Protocol/Features/Options/FileBasedAppsOptionsStorage.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.Options; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +internal static class FileBasedAppsOptionsStorage +{ + private static readonly OptionGroup s_optionGroup = new(name: "file_based_apps", description: ""); + + /// + /// Whether to automatically discover and load file-based app entry points. + /// + public static readonly Option2 EnableAutomaticDiscovery = new("dotnet_enable_automatic_discovery", defaultValue: true, s_optionGroup); +} diff --git a/src/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs b/src/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs index 987bd3a2080d..94f20e690ab4 100644 --- a/src/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs +++ b/src/LanguageServer/Protocol/Features/Options/LanguageServerProjectSystemOptionsStorage.cs @@ -26,7 +26,7 @@ internal static class LanguageServerProjectSystemOptionsStorage public static readonly Option2 EnableFileBasedPrograms = new("dotnet_enable_file_based_programs", defaultValue: true, s_optionGroup); /// - /// Whether to use the new 'dotnet run app.cs' (file-based programs) experience in files where the editor is unable to determine with certainty whether the file is a file-based program. + /// Controls whether to show semantic errors in miscellaneous files with top-level statements and no #: directives. /// - public static readonly Option2 EnableFileBasedProgramsWhenAmbiguous = new("dotnet_enable_file_based_programs_when_ambiguous", defaultValue: true, s_optionGroup); + public static readonly Option2 EnableSemanticErrorsInMiscellaneousFiles = new("dotnet_enable_file_based_programs_when_ambiguous", defaultValue: true, s_optionGroup); } diff --git a/src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs b/src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs index a0411880f871..b7da11a12831 100644 --- a/src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs +++ b/src/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs @@ -49,10 +49,9 @@ public AbstractRefreshQueue( providerRefresher.ProviderRefreshRequested += EnqueueRefreshNotification; } - public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) { Initialize(clientCapabilities); - return Task.CompletedTask; } public void Initialize(ClientCapabilities clientCapabilities) diff --git a/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyHelpers.cs b/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyHelpers.cs new file mode 100644 index 000000000000..18dda6dd203c --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyHelpers.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CallHierarchy; +using Microsoft.CodeAnalysis.Text; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CallHierarchy; + +internal static class CallHierarchyHelpers +{ + public static CallHierarchyResolveData GetResolveData(LSP.CallHierarchyItem item) + { + Contract.ThrowIfNull(item.Data); + var resolveData = JsonSerializer.Deserialize((JsonElement)item.Data, ProtocolConversions.LspJsonSerializerOptions); + Contract.ThrowIfNull(resolveData, "Missing data for call hierarchy request"); + return resolveData; + } + + public static async Task CreateItemAsync( + CallHierarchyItemDescriptor descriptor, + Solution solution, + CancellationToken cancellationToken) + => await CreateItemAsync(descriptor, solution, preferredDocumentId: null, cancellationToken).ConfigureAwait(false); + + public static async Task CreateItemAsync( + CallHierarchyItemDescriptor descriptor, + Solution solution, + DocumentId? preferredDocumentId, + CancellationToken cancellationToken) + { + var resolved = await descriptor.ItemId.TryResolveAsync(solution, cancellationToken).ConfigureAwait(false); + if (resolved == null) + return null; + + var (symbol, _) = resolved.Value; + var sourceInfo = await GetSourceInfoAsync(symbol, solution, preferredDocumentId, cancellationToken).ConfigureAwait(false); + if (sourceInfo == null) + return null; + + var (document, declarationSpan, selectionSpan) = sourceInfo.Value; + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + + return new LSP.CallHierarchyItem + { + Name = GetName(descriptor), + Kind = ProtocolConversions.GlyphToSymbolKind(descriptor.Glyph), + Detail = GetDetail(descriptor), + Uri = document.GetURI(), + Range = ProtocolConversions.TextSpanToRange(declarationSpan, text), + SelectionRange = ProtocolConversions.TextSpanToRange(selectionSpan, text), + Data = new CallHierarchyResolveData(descriptor.ItemId.SymbolKeyData, descriptor.ItemId.ProjectId.Id, ProtocolConversions.DocumentToTextDocumentIdentifier(document)), + }; + + static string GetName(CallHierarchyItemDescriptor descriptor) + { + return string.IsNullOrEmpty(descriptor.ContainingTypeName) + ? descriptor.MemberName + : $"{descriptor.ContainingTypeName}.{descriptor.MemberName}"; + } + + static string? GetDetail(CallHierarchyItemDescriptor descriptor) + { + if (string.IsNullOrEmpty(descriptor.ContainingTypeName)) + return string.IsNullOrEmpty(descriptor.ContainingNamespaceName) ? null : descriptor.ContainingNamespaceName; + + return string.IsNullOrEmpty(descriptor.ContainingNamespaceName) + ? descriptor.ContainingTypeName + : $"{descriptor.ContainingNamespaceName}.{descriptor.ContainingTypeName}"; + } + } + + public static async Task> ConvertLocationsToRangesAsync( + ImmutableArray locations, + Document document, + CancellationToken cancellationToken) + { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + return [.. locations.Where(static location => location.IsInSource).Select(location => ProtocolConversions.TextSpanToRange(location.SourceSpan, text))]; + } + + private static async Task<(Document Document, TextSpan DeclarationSpan, TextSpan SelectionSpan)?> GetSourceInfoAsync( + ISymbol symbol, + Solution solution, + DocumentId? preferredDocumentId, + CancellationToken cancellationToken) + { + var sourceInfo = await TryGetSourceInfoAsync(symbol, solution, preferredDocumentId, cancellationToken).ConfigureAwait(false); + + if (sourceInfo == null && symbol is IMethodSymbol { IsImplicitlyDeclared: true, MethodKind: MethodKind.Constructor or MethodKind.StaticConstructor } methodSymbol) + { + // Implicit constructors don't exist in source, so try to get the source info for the containing type instead. + sourceInfo = await TryGetSourceInfoAsync(methodSymbol.ContainingType, solution, preferredDocumentId, cancellationToken).ConfigureAwait(false); + } + + return sourceInfo; + } + + private static async Task<(Document Document, TextSpan DeclarationSpan, TextSpan SelectionSpan)?> TryGetSourceInfoAsync( + ISymbol symbol, + Solution solution, + DocumentId? preferredDocumentId, + CancellationToken cancellationToken) + { + foreach (var syntaxReference in symbol.DeclaringSyntaxReferences) + { + var document = solution.GetDocument(syntaxReference.SyntaxTree); + if (document == null || (preferredDocumentId != null && document.Id != preferredDocumentId)) + continue; + + var syntax = await syntaxReference.GetSyntaxAsync(cancellationToken).ConfigureAwait(false); + var declarationSpan = syntax.Span; + var selectionSpan = symbol.Locations.FirstOrDefault(location => + location.IsInSource && + location.SourceTree == syntax.SyntaxTree && + declarationSpan.Contains(location.SourceSpan))?.SourceSpan ?? declarationSpan; + + return (document, declarationSpan, selectionSpan); + } + + return null; + } +} diff --git a/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyIncomingCallsHandler.cs b/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyIncomingCallsHandler.cs new file mode 100644 index 000000000000..3be4a7b2d72a --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyIncomingCallsHandler.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CallHierarchy; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.LanguageServer.Protocol; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CallHierarchy; + +[ExportCSharpVisualBasicStatelessLspService(typeof(CallHierarchyIncomingCallsHandler)), Shared] +[Method(LSP.Methods.CallHierarchyIncomingCallsName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CallHierarchyIncomingCallsHandler() : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CallHierarchyIncomingCallsParams request) + => CallHierarchyHelpers.GetResolveData(request.Item).TextDocument; + + public async Task HandleRequestAsync(LSP.CallHierarchyIncomingCallsParams request, RequestContext context, CancellationToken cancellationToken) + { + var document = context.GetRequiredDocument(); + var solution = document.Project.Solution; + var resolveData = CallHierarchyHelpers.GetResolveData(request.Item); + + var service = document.GetRequiredLanguageService(); + var results = await service.SearchIncomingCallsAsync( + solution, + new CallHierarchySearchDescriptor(CallHierarchyRelationshipKind.Callers, resolveData.GetItemId()), + documents: null, + cancellationToken).ConfigureAwait(false); + + var incomingCalls = new List(); + foreach (var result in results.Where(result => result.Item != null)) + { + var locationsByDocumentId = result.ReferenceLocations + .Where(static location => location.IsInSource) + .GroupBy(location => solution.GetDocument(location.SourceTree)?.Id); + + foreach (var locationGroup in locationsByDocumentId) + { + if (locationGroup.Key == null) + continue; + + var callerDocument = solution.GetDocument(locationGroup.Key); + if (callerDocument == null) + continue; + + var fromItem = await CallHierarchyHelpers.CreateItemAsync(result.Item!, solution, locationGroup.Key, cancellationToken).ConfigureAwait(false); + if (fromItem == null) + continue; + + var ranges = await CallHierarchyHelpers.ConvertLocationsToRangesAsync([.. locationGroup], callerDocument, cancellationToken).ConfigureAwait(false); + incomingCalls.Add(new LSP.CallHierarchyIncomingCall + { + From = fromItem, + FromRanges = [.. ranges], + }); + } + } + + return [.. incomingCalls]; + } +} diff --git a/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyOutgoingCallsHandler.cs b/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyOutgoingCallsHandler.cs new file mode 100644 index 000000000000..9628ba08b156 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyOutgoingCallsHandler.cs @@ -0,0 +1,63 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CallHierarchy; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.LanguageServer.Protocol; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CallHierarchy; + +[ExportCSharpVisualBasicStatelessLspService(typeof(CallHierarchyOutgoingCallsHandler)), Shared] +[Method(LSP.Methods.CallHierarchyOutgoingCallsName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class CallHierarchyOutgoingCallsHandler() : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CallHierarchyOutgoingCallsParams request) + => CallHierarchyHelpers.GetResolveData(request.Item).TextDocument; + + public async Task HandleRequestAsync(LSP.CallHierarchyOutgoingCallsParams request, RequestContext context, CancellationToken cancellationToken) + { + var document = context.GetRequiredDocument(); + var solution = document.Project.Solution; + var resolveData = CallHierarchyHelpers.GetResolveData(request.Item); + + var service = document.GetRequiredLanguageService(); + var results = await service.SearchOutgoingCallsAsync( + solution, + resolveData.GetItemId(), + ImmutableHashSet.Create(document), + cancellationToken).ConfigureAwait(false); + + var outgoingCalls = new List(); + foreach (var result in results.Where(result => result.Item != null)) + { + var toItem = await CallHierarchyHelpers.CreateItemAsync(result.Item!, solution, cancellationToken).ConfigureAwait(false); + if (toItem == null) + continue; + + var ranges = await CallHierarchyHelpers.ConvertLocationsToRangesAsync(result.ReferenceLocations, document, cancellationToken).ConfigureAwait(false); + outgoingCalls.Add(new LSP.CallHierarchyOutgoingCall + { + To = toItem, + FromRanges = [.. ranges], + }); + } + + return [.. outgoingCalls]; + } +} diff --git a/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyResolveData.cs b/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyResolveData.cs new file mode 100644 index 000000000000..48de6d9889a5 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/CallHierarchy/CallHierarchyResolveData.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis.CallHierarchy; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CallHierarchy; + +internal sealed record CallHierarchyResolveData( + string SymbolKeyData, + Guid ProjectGuid, + TextDocumentIdentifier TextDocument) : DocumentResolveData(TextDocument) +{ + public CallHierarchyItemId GetItemId() + => new(SymbolKeyData, ProjectId.CreateFromSerialized(ProjectGuid)); +} diff --git a/src/LanguageServer/Protocol/Handler/CallHierarchy/PrepareCallHierarchyHandler.cs b/src/LanguageServer/Protocol/Handler/CallHierarchy/PrepareCallHierarchyHandler.cs new file mode 100644 index 000000000000..29c6f78059ff --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/CallHierarchy/PrepareCallHierarchyHandler.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CallHierarchy; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.LanguageServer.Protocol; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CallHierarchy; + +[ExportCSharpVisualBasicStatelessLspService(typeof(PrepareCallHierarchyHandler)), Shared] +[Method(LSP.Methods.PrepareCallHierarchyName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class PrepareCallHierarchyHandler() : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CallHierarchyPrepareParams request) + => request.TextDocument; + + public async Task HandleRequestAsync(LSP.CallHierarchyPrepareParams request, RequestContext context, CancellationToken cancellationToken) + { + var document = context.GetRequiredDocument(); + var solution = document.Project.Solution; + var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(semanticModel, position, solution.Services, includeType: true, cancellationToken).ConfigureAwait(false); + if (symbol == null) + return null; + + var service = document.GetRequiredLanguageService(); + var itemDescriptor = await service.CreateItemAsync(symbol, document.Project, cancellationToken).ConfigureAwait(false); + if (itemDescriptor == null) + return null; + + var item = await CallHierarchyHelpers.CreateItemAsync(itemDescriptor, solution, preferredDocumentId: document.Id, cancellationToken).ConfigureAwait(false); + return item == null ? null : [item]; + } +} diff --git a/src/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs b/src/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs index cbc1013c1a72..9a23db184c14 100644 --- a/src/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs +++ b/src/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs @@ -58,7 +58,7 @@ internal sealed partial class DidChangeConfigurationNotificationHandler LanguageServerProjectSystemOptionsStorage.BinaryLogPath, LanguageServerProjectSystemOptionsStorage.EnableAutomaticRestore, LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms, - LanguageServerProjectSystemOptionsStorage.EnableFileBasedProgramsWhenAmbiguous, + LanguageServerProjectSystemOptionsStorage.EnableSemanticErrorsInMiscellaneousFiles, MetadataAsSourceOptionsStorage.NavigateToSourceLinkAndEmbeddedSources, LspOptionsStorage.LspOrganizeImportsOnFormat, ]; diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs index 00d487b59e98..d691d1e10a33 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs @@ -3,13 +3,16 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; +using Microsoft.VisualStudio.Threading; using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -23,16 +26,11 @@ internal abstract class AbstractWorkspacePullDiagnosticsHandler - /// Gate to guard access to - ///
- private readonly object _gate = new(); - /// /// Stores the LSP changed state on a per category basis. This ensures that requests for different categories /// are 'walled off' from each other and only reset state for their own category. /// - private readonly Dictionary _categoryToLspChanged = []; + private readonly ConcurrentDictionary _categoryToLspChanged = []; protected AbstractWorkspacePullDiagnosticsHandler( LspWorkspaceManager workspaceManager, @@ -89,14 +87,9 @@ private void OnWorkspaceRefreshRequested() private void UpdateLspChanged() { - lock (_gate) - { - // Loop through our map of source -> has changed and mark them as all having changed. - foreach (var category in _categoryToLspChanged.Keys.ToImmutableArray()) - { - _categoryToLspChanged[category] = true; - } - } + // Loop through our map of source -> has changed and mark them as all having changed. + foreach (var categoryResetEvent in _categoryToLspChanged.Values) + categoryResetEvent.Set(); } protected override async Task WaitForChangesAsync(string? category, RequestContext context, CancellationToken cancellationToken) @@ -104,41 +97,85 @@ protected override async Task WaitForChangesAsync(string? category, RequestConte // A null category counts a separate category and should track changes independently of other categories, so we'll add an empty entry in our map for it. category ??= string.Empty; - // Spin waiting until our LSP change flag has been set. When the flag is set (meaning LSP has changed), - // we reset the flag to false and exit out of the loop allowing the request to close. - // The client will automatically trigger a new request as soon as we close it, bringing us up to date on diagnostics. - while (!HasChanged()) - { - // There have been no changes between now and when the last request finished - we will hold the connection open while we poll for changes. - await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken).ConfigureAwait(false); - } + // Wait until the workspace changes again (or was changed while we were in the middle of processing). + // We'll use a variant of an AutoResetEvent so in the case we were to have multiple requests for the same category, + // they're all released. That's not expected to happen, but it ensures better behavior in the case of a misbehaving client. + var resetEvent = _categoryToLspChanged.GetOrAdd(category, static _ => new ReleaseAllAutoResetEvent(initialState: true)); + await resetEvent.WaitAsync().WithCancellation(cancellationToken).ConfigureAwait(false); // We've hit a change, so we close the current request to allow the client to open a new one. context.TraceDebug($"Closing workspace/diagnostics request for {category}"); - return; + } + + internal abstract TestAccessor GetTestAccessor(); + + internal readonly struct TestAccessor(AbstractWorkspacePullDiagnosticsHandler handler) + { + public void TriggerConnectionClose() => handler.UpdateLspChanged(); + } + + /// + /// An with two differences: it supports async waiting, and in the case Set() releases a waiter, it releases all waiters rather than just one. + /// + /// The semantics of this type are thus: there is internally a "set" state. When the event is , the next waiter to call will be let through, and the + /// event resets to false. A call to while there is already waiters will release all the waiters, and since it already let a waiter through, the state is untouched. + /// + private sealed class ReleaseAllAutoResetEvent + { + private readonly object _gate = new object(); + private readonly List> _waiters = new(); + + /// + /// True if has been called, indicating the next waiter should be let through. + /// + private bool _state; - bool HasChanged() + public ReleaseAllAutoResetEvent(bool initialState) + { + _state = initialState; + } + + public Task WaitAsync() { lock (_gate) { - // Get the LSP changed value of this category. If it doesn't exist we add it with a value of 'changed' since this is the first - // request for the category and we don't know if it has changed since the request started. - var changed = _categoryToLspChanged.GetOrAdd(category, true); - if (changed) + if (_state) { - // We've observed a change, so we reset the flag to false for this source and return true. - _categoryToLspChanged[category] = false; + // Since _state was true, we let the next waiter through. Any waiter after that must wait for the next Set(). + Contract.ThrowIfTrue(_waiters.Count > 0); + _state = false; + return Task.CompletedTask; + } + else + { + // Passing RunContinuationsAsynchronously ensures we can call SetResult() on this without any risk of the things running inside the lock. + var waiter = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _waiters.Add(waiter); + return waiter.Task; } - - return changed; } } - } - internal abstract TestAccessor GetTestAccessor(); + public void Set() + { + lock (_gate) + { + if (_waiters.Count > 0) + { + // We had some waiters waiting, so _state should be false. We'll let all the waiters through. + Contract.ThrowIfTrue(_state); - internal readonly struct TestAccessor(AbstractWorkspacePullDiagnosticsHandler handler) - { - public void TriggerConnectionClose() => handler.UpdateLspChanged(); + foreach (var waiter in _waiters) + waiter.SetResult(null); + + _waiters.Clear(); + } + else + { + // There are no waiters, so we'll let the next waiter through when they call WaitAsync(). + _state = true; + } + } + } } } diff --git a/src/LanguageServer/Protocol/Handler/DocumentChanges/DidSaveHandler.cs b/src/LanguageServer/Protocol/Handler/DocumentChanges/DidSaveHandler.cs new file mode 100644 index 000000000000..fcfc82546a6a --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/DocumentChanges/DidSaveHandler.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.DocumentChanges; + +[ExportCSharpVisualBasicStatelessLspService(typeof(DidSaveHandler)), Shared] +[Method(Methods.TextDocumentDidSaveName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class DidSaveHandler() : ILspServiceNotificationHandler, ITextDocumentIdentifierHandler +{ + public bool MutatesSolutionState => false; + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(DidSaveTextDocumentParams request) => request.TextDocument; + + public async Task HandleNotificationAsync(DidSaveTextDocumentParams request, RequestContext context, CancellationToken cancellationToken) + { + var document = context.GetRequiredDocument(); + var workspace = document.Project.Solution.Workspace; + context.TraceDebug($"RefreshSourceGenerators for {document.Project.Id.DebugName}"); + + workspace.EnqueueUpdateSourceGeneratorVersion(document.Project.Id, forceRegeneration: false); + } +} \ No newline at end of file diff --git a/src/LanguageServer/Protocol/Handler/IInitializeManager.cs b/src/LanguageServer/Protocol/Handler/IInitializeManager.cs index 195fa6c63d33..8e724a20f98b 100644 --- a/src/LanguageServer/Protocol/Handler/IInitializeManager.cs +++ b/src/LanguageServer/Protocol/Handler/IInitializeManager.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Immutable; using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler; @@ -14,5 +15,8 @@ internal interface IInitializeManager : ILspService InitializeParams? TryGetInitializeParams(); + /// Expected to be non-default after the Initialize event. + ImmutableArray GetRequiredWorkspaceFolderPaths(); + void SetInitializeParams(InitializeParams initializeParams); } diff --git a/src/LanguageServer/Protocol/Handler/InitializeManager.cs b/src/LanguageServer/Protocol/Handler/InitializeManager.cs index be47ac8ac17b..c4339d3de5eb 100644 --- a/src/LanguageServer/Protocol/Handler/InitializeManager.cs +++ b/src/LanguageServer/Protocol/Handler/InitializeManager.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler; @@ -14,6 +16,7 @@ public InitializeManager() } private InitializeParams? _initializeParams; + private ImmutableArray _workspaceFolderPathsOpt; public ClientCapabilities GetClientCapabilities() { @@ -29,6 +32,22 @@ public void SetInitializeParams(InitializeParams initializeParams) { Contract.ThrowIfFalse(_initializeParams == null); _initializeParams = initializeParams; + _workspaceFolderPathsOpt = initializeParams.WorkspaceFolders is [_, ..] workspaceFolders ? GetFolderPaths(workspaceFolders) : []; + + static ImmutableArray GetFolderPaths(WorkspaceFolder[] workspaceFolders) + { + var builder = ArrayBuilder.GetInstance(workspaceFolders.Length); + foreach (var workspaceFolder in workspaceFolders) + { + if (workspaceFolder.DocumentUri.ParsedUri is not { } parsedUri) + continue; + + var workspaceFolderPath = ProtocolConversions.GetDocumentFilePathFromUri(parsedUri); + builder.Add(workspaceFolderPath); + } + + return builder.ToImmutableAndFree(); + } } public InitializeParams? TryGetInitializeParams() @@ -36,6 +55,12 @@ public void SetInitializeParams(InitializeParams initializeParams) return _initializeParams; } + public ImmutableArray GetRequiredWorkspaceFolderPaths() + { + Contract.ThrowIfTrue(_workspaceFolderPathsOpt.IsDefault, $"{nameof(_workspaceFolderPathsOpt)} was not initialized. Was this accessed before the OnInitialized event ran?"); + return _workspaceFolderPathsOpt; + } + public ClientCapabilities? TryGetClientCapabilities() { return _initializeParams?.Capabilities; diff --git a/src/LanguageServer/Protocol/Handler/References/FindImplementationsHandler.cs b/src/LanguageServer/Protocol/Handler/References/FindImplementationsHandler.cs index 8a4ff359e3f9..ca120d45a7dc 100644 --- a/src/LanguageServer/Protocol/Handler/References/FindImplementationsHandler.cs +++ b/src/LanguageServer/Protocol/Handler/References/FindImplementationsHandler.cs @@ -59,13 +59,20 @@ public FindImplementationsHandler(IGlobalOptionService globalOptions) var text = definition.GetClassifiedText(); foreach (var sourceSpan in definition.SourceSpans) { + // Use a zero-length span at the start of the source span to navigate to a + // position rather than selecting the entire span. + // Navigating to a span selects the text, which regresses screen readers (which then only read + // the selected word instead of the whole line). + // Additionally, since results are not live, spans may grow stale after edits - navigating to a position avoids a bogus selection of arbitrary text. + // See https://github.com/dotnet/roslyn/pull/75418 + var positionSpan = new DocumentSpan(sourceSpan.Document, new TextSpan(sourceSpan.SourceSpan.Start, 0)); if (supportsVisualStudioExtensions) { - locations.AddIfNotNull(await ProtocolConversions.DocumentSpanToLocationWithTextAsync(sourceSpan, text, cancellationToken).ConfigureAwait(false)); + locations.AddIfNotNull(await ProtocolConversions.DocumentSpanToLocationWithTextAsync(positionSpan, text, cancellationToken).ConfigureAwait(false)); } else { - locations.AddIfNotNull(await ProtocolConversions.DocumentSpanToLocationAsync(sourceSpan, cancellationToken).ConfigureAwait(false)); + locations.AddIfNotNull(await ProtocolConversions.DocumentSpanToLocationAsync(positionSpan, cancellationToken).ConfigureAwait(false)); } } } diff --git a/src/LanguageServer/Protocol/Handler/RequestContextFactory.cs b/src/LanguageServer/Protocol/Handler/RequestContextFactory.cs index fddba24fcac4..cf946b140dd6 100644 --- a/src/LanguageServer/Protocol/Handler/RequestContextFactory.cs +++ b/src/LanguageServer/Protocol/Handler/RequestContextFactory.cs @@ -19,7 +19,7 @@ public RequestContextFactory(ILspServices lspServices) _lspServices = lspServices; } - public override Task CreateRequestContextAsync(IQueueItem queueItem, IMethodHandler methodHandler, TRequestParam requestParam, CancellationToken cancellationToken) + public override Task CreateRequestContextAsync(QueueItem queueItem, IMethodHandler methodHandler, TRequestParam requestParam, CancellationToken cancellationToken) { var clientCapabilitiesManager = _lspServices.GetRequiredService(); var clientCapabilities = clientCapabilitiesManager.TryGetClientCapabilities(); diff --git a/src/LanguageServer/Protocol/Handler/SelectionRanges/SelectionRangeHandler.cs b/src/LanguageServer/Protocol/Handler/SelectionRanges/SelectionRangeHandler.cs new file mode 100644 index 000000000000..468cfaf4d70c --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/SelectionRanges/SelectionRangeHandler.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler; + +[ExportCSharpVisualBasicStatelessLspService(typeof(SelectionRangeHandler)), Shared] +[Method(Methods.TextDocumentSelectionRangeName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class SelectionRangeHandler() : ILspServiceDocumentRequestHandler +{ + public bool MutatesSolutionState => false; + public bool RequiresLSPSolution => true; + + public TextDocumentIdentifier GetTextDocumentIdentifier(SelectionRangeParams request) => request.TextDocument; + + public async Task HandleRequestAsync(SelectionRangeParams request, RequestContext context, CancellationToken cancellationToken) + { + var document = context.Document; + if (document is null) + return null; + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + + using var _ = ArrayBuilder.GetInstance(out var results); + foreach (var position in request.Positions) + { + var linePosition = ProtocolConversions.PositionToLinePosition(position); + var absolutePosition = text.Lines.GetPosition(linePosition); + + results.Add(GetSelectionRange(root, text, absolutePosition)); + } + + return results.ToArray(); + } + + private static SelectionRange GetSelectionRange(SyntaxNode root, SourceText text, int position) + { + // FindToken().Parent is null only for EOF tokens at the compilation unit level; + // falling back to root ensures we still return a valid selection range in that case. + var node = root.FindToken(position).Parent ?? root; + + // Collect spans from innermost to outermost, deduplicating consecutive equal spans. + using var _ = ArrayBuilder.GetInstance(out var spans); + var previousSpan = (TextSpan?)null; + foreach (var ancestor in node.AncestorsAndSelf()) + { + var span = ancestor.Span; + + // Skip nodes with empty spans and deduplicate nodes that cover the same span. + if (span.IsEmpty || span == previousSpan) + continue; + + spans.Add(span); + previousSpan = span; + } + + // Build the SelectionRange linked list from outermost to innermost so that each + // SelectionRange's Parent refers to a larger enclosing range, as the LSP spec requires. + SelectionRange? current = null; + for (var i = spans.Count - 1; i >= 0; i--) + { + current = new SelectionRange + { + Range = ProtocolConversions.TextSpanToRange(spans[i], text), + Parent = current + }; + } + + // If we somehow ended up with nothing (e.g. empty file), return an empty range at the position. + return current ?? new SelectionRange { Range = ProtocolConversions.TextSpanToRange(new TextSpan(position, 0), text) }; + } +} diff --git a/src/LanguageServer/Protocol/Handler/ServerLifetime/InitializeHandler.cs b/src/LanguageServer/Protocol/Handler/ServerLifetime/InitializeHandler.cs index 0ecf18b587d5..13ef10d46098 100644 --- a/src/LanguageServer/Protocol/Handler/ServerLifetime/InitializeHandler.cs +++ b/src/LanguageServer/Protocol/Handler/ServerLifetime/InitializeHandler.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -23,6 +22,9 @@ public async Task HandleRequestAsync(InitializeParams request, var clientCapabilities = request.Capabilities; clientCapabilitiesManager.SetInitializeParams(request); + if (request.ProcessId is int clientProcessId && RoslynLanguageServer.TryRegisterClientProcessId(clientProcessId)) + context.Logger.LogInformation("Monitoring client process {clientProcessId} for exit", clientProcessId); + var capabilitiesProvider = context.GetRequiredLspService(); var serverCapabilities = capabilitiesProvider.GetCapabilities(clientCapabilities); @@ -34,9 +36,10 @@ public async Task HandleRequestAsync(InitializeParams request, m["capabilities"] = JsonSerializer.Serialize(serverCapabilities, ProtocolConversions.LspJsonSerializerOptions); })); - return new InitializeResult + return new RoslynInitializeResult { Capabilities = serverCapabilities, + ProcessId = RoslynLanguageServer.ServerProcessId, }; } } diff --git a/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs b/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs index 0c224504a535..9cce75d01e67 100644 --- a/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs +++ b/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs @@ -38,7 +38,7 @@ private LspServiceLifeCycleManager(IClientLanguageServerManager clientLanguageSe _lspWorkspaceRegistrationService = lspWorkspaceRegistrationService; } - public async Task ShutdownAsync(string message = "Shutting down") + public async Task ShutdownAsync() { // Shutting down is not cancellable. var cancellationToken = CancellationToken.None; @@ -52,20 +52,6 @@ public async Task ShutdownAsync(string message = "Shutting down") var service = hostWorkspace.Services.GetRequiredService(); await service.ResetAsync(cancellationToken).ConfigureAwait(false); } - - try - { - var messageParams = new LogMessageParams() - { - MessageType = MessageType.Info, - Message = message - }; - await _clientLanguageServerManager.SendNotificationAsync("window/logMessage", messageParams, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) when (ex is ObjectDisposedException or ConnectionLostException) - { - //Don't fail shutdown just because jsonrpc has already been cancelled. - } } public async Task ExitAsync() diff --git a/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratorRefreshQueue.cs b/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratorRefreshQueue.cs index 1e2cf287c54e..cbfad43eaac3 100644 --- a/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratorRefreshQueue.cs +++ b/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratorRefreshQueue.cs @@ -50,17 +50,16 @@ public SourceGeneratorRefreshQueue( _disposalTokenSource.Token); } - public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) { if (clientCapabilities.HasVisualStudioLspCapability()) { // VS source generated document content is not provided by LSP. - return Task.CompletedTask; + return; } // After we have initialized we can start listening for workspace changes. _lspWorkspaceRegistrationService.LspSolutionChanged += OnLspSolutionChanged; - return Task.CompletedTask; } private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e) diff --git a/src/LanguageServer/Protocol/Handler/SourceGenerators/WorkspaceRefreshSourceGeneratorsHandler.cs b/src/LanguageServer/Protocol/Handler/SourceGenerators/WorkspaceRefreshSourceGeneratorsHandler.cs new file mode 100644 index 000000000000..56e210c7115f --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/SourceGenerators/WorkspaceRefreshSourceGeneratorsHandler.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler; + +/// +/// Handles a request from the client to refresh source generators. +/// No specific generators are refreshed; rather, all generators are refreshed in all registered workspaces. +/// +[ExportCSharpVisualBasicStatelessLspService(typeof(WorkspaceRefreshSourceGeneratorsHandler)), Shared] +[Method(MethodName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class WorkspaceRefreshSourceGeneratorsHandler(LspWorkspaceRegistrationService workspaceRegistrationService) : ILspServiceNotificationHandler +{ + public const string MethodName = "workspace/_roslyn_refreshSourceGenerators"; + + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => false; + + public Task HandleNotificationAsync(RefreshSourceGeneratorsParams request, RequestContext requestContext, CancellationToken cancellationToken) + { + foreach (var workspace in workspaceRegistrationService.GetAllRegistrations()) + { + workspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, request.ForceRegeneration); + } + + return Task.CompletedTask; + } +} + +internal record RefreshSourceGeneratorsParams( + [property: JsonPropertyName("forceRegeneration")] bool ForceRegeneration +); diff --git a/src/LanguageServer/Protocol/Handler/WorkDoneProgress/WorkDoneProgressManager.cs b/src/LanguageServer/Protocol/Handler/WorkDoneProgress/WorkDoneProgressManager.cs index b835a6ce7415..ff37c97a0d26 100644 --- a/src/LanguageServer/Protocol/Handler/WorkDoneProgress/WorkDoneProgressManager.cs +++ b/src/LanguageServer/Protocol/Handler/WorkDoneProgress/WorkDoneProgressManager.cs @@ -16,9 +16,10 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler; /// Manages server initiated work done progress reporting to the client. /// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverInitiatedProgress /// -class WorkDoneProgressManager(IClientLanguageServerManager clientLanguageServerManager) : ILspService +class WorkDoneProgressManager(IClientLanguageServerManager clientLanguageServerManager, IInitializeManager initializeManager) : ILspService { private readonly IClientLanguageServerManager _clientLanguageServerManager = clientLanguageServerManager; + private readonly IInitializeManager _initializeManager = initializeManager; /// /// Guards access to . @@ -51,7 +52,11 @@ public async Task CreateWorkDoneProgressAsync(bool re { var token = Guid.NewGuid().ToString(); IWorkDoneProgressReporter reporter; - if (reportProgressToClient) + + // Only report progress to the client if both the client advertised support for progress reporting and the caller requested it. + var reportProgress = reportProgressToClient && _initializeManager.GetClientCapabilities().Window?.WorkDoneProgress == true; + + if (reportProgress) { var clientReporter = new WorkDoneProgressReporter(token, this, serverCancellationToken); await clientReporter.SendCreateRequestAsync().ConfigureAwait(false); diff --git a/src/LanguageServer/Protocol/Handler/WorkDoneProgress/WorkDoneProgressManagerFactory.cs b/src/LanguageServer/Protocol/Handler/WorkDoneProgress/WorkDoneProgressManagerFactory.cs index 8cf293aeb9fe..ab38a2a390e7 100644 --- a/src/LanguageServer/Protocol/Handler/WorkDoneProgress/WorkDoneProgressManagerFactory.cs +++ b/src/LanguageServer/Protocol/Handler/WorkDoneProgress/WorkDoneProgressManagerFactory.cs @@ -16,6 +16,7 @@ internal sealed class WorkDoneProgressManagerFactory() : ILspServiceFactory public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var clientLanguageServerManager = lspServices.GetRequiredService(); - return new WorkDoneProgressManager(clientLanguageServerManager); + var initializeManager = lspServices.GetRequiredService(); + return new WorkDoneProgressManager(clientLanguageServerManager, initializeManager); } } \ No newline at end of file diff --git a/src/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj b/src/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj index 128285a30a35..345fdddb32a3 100644 --- a/src/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj +++ b/src/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj @@ -91,8 +91,6 @@ - - diff --git a/src/LanguageServer/Protocol/Protocol/InitializeResult.cs b/src/LanguageServer/Protocol/Protocol/InitializeResult.cs index 2a766aff1077..27f3e449b52c 100644 --- a/src/LanguageServer/Protocol/Protocol/InitializeResult.cs +++ b/src/LanguageServer/Protocol/Protocol/InitializeResult.cs @@ -11,7 +11,7 @@ namespace Roslyn.LanguageServer.Protocol; /// /// See the Language Server Protocol specification for additional information. /// -internal sealed class InitializeResult +internal class InitializeResult { /// /// Gets or sets the server capabilities. diff --git a/src/LanguageServer/Protocol/Protocol/Internal/RoslynInitializeResult.cs b/src/LanguageServer/Protocol/Protocol/Internal/RoslynInitializeResult.cs new file mode 100644 index 000000000000..3d780567f76e --- /dev/null +++ b/src/LanguageServer/Protocol/Protocol/Internal/RoslynInitializeResult.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Roslyn.LanguageServer.Protocol; + +using System.Text.Json.Serialization; + +internal sealed class RoslynInitializeResult : InitializeResult +{ + [JsonPropertyName("_roslyn_processId")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public int ProcessId { get; set; } +} diff --git a/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyIncomingCallsParams.cs b/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyIncomingCallsParams.cs index b2c27eaf0e4e..ba00e3b5233b 100644 --- a/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyIncomingCallsParams.cs +++ b/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyIncomingCallsParams.cs @@ -14,7 +14,7 @@ namespace Roslyn.LanguageServer.Protocol; /// /// /// Since LSP 3.16 -internal sealed class CallHierarchyIncomingCallsParams : TextDocumentPositionParams, IWorkDoneProgressParams, IPartialResultParams +internal sealed class CallHierarchyIncomingCallsParams : IWorkDoneProgressParams, IPartialResultParams { /// /// The item returned from `textDocument/prepareCallHierarchy` diff --git a/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyOutgoingCallsParams.cs b/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyOutgoingCallsParams.cs index c0297ac170bf..19ed1b6773cc 100644 --- a/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyOutgoingCallsParams.cs +++ b/src/LanguageServer/Protocol/Protocol/Navigation/CallHierarchyOutgoingCallsParams.cs @@ -14,7 +14,7 @@ namespace Roslyn.LanguageServer.Protocol; /// /// /// Since LSP 3.16 -internal sealed class CallHierarchyOutgoingCallsParams : TextDocumentPositionParams, IWorkDoneProgressParams, IPartialResultParams +internal sealed class CallHierarchyOutgoingCallsParams : IWorkDoneProgressParams, IPartialResultParams { /// /// The item returned from `textDocument/prepareCallHierarchy` diff --git a/src/LanguageServer/Protocol/Protocol/SelectionRange.cs b/src/LanguageServer/Protocol/Protocol/SelectionRange.cs index 619778661e2a..e1f03f362b12 100644 --- a/src/LanguageServer/Protocol/Protocol/SelectionRange.cs +++ b/src/LanguageServer/Protocol/Protocol/SelectionRange.cs @@ -29,6 +29,6 @@ internal sealed class SelectionRange /// /// [JsonPropertyName("parent")] - [JsonRequired] - public SelectionRange Parent { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public SelectionRange? Parent { get; init; } } diff --git a/src/LanguageServer/Protocol/Protocol/SelectionRangeParams.cs b/src/LanguageServer/Protocol/Protocol/SelectionRangeParams.cs index 6ce55156a2e8..91957c519fc9 100644 --- a/src/LanguageServer/Protocol/Protocol/SelectionRangeParams.cs +++ b/src/LanguageServer/Protocol/Protocol/SelectionRangeParams.cs @@ -26,7 +26,7 @@ internal sealed class SelectionRangeParams : ITextDocumentParams, IWorkDoneProgr /// /// The positions inside the text document. /// - [JsonPropertyName("textDocument")] + [JsonPropertyName("positions")] [JsonRequired] public Position[] Positions { get; init; } diff --git a/src/LanguageServer/Protocol/Protocol/ServerCapabilities.cs b/src/LanguageServer/Protocol/Protocol/ServerCapabilities.cs index e986cbc6bc3a..2ef8fe5e28b0 100644 --- a/src/LanguageServer/Protocol/Protocol/ServerCapabilities.cs +++ b/src/LanguageServer/Protocol/Protocol/ServerCapabilities.cs @@ -213,7 +213,7 @@ public TextDocumentSyncOptions? TextDocumentSync /// Since LSP 3.15 [JsonPropertyName("selectionRangeProvider")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public SumType? SelectionRangeProvider { get; init; } + public SumType? SelectionRangeProvider { get; set; } /// /// Gets or sets a value indicating whether the server supports linked editing range. @@ -229,7 +229,7 @@ public TextDocumentSyncOptions? TextDocumentSync /// Since LSP 3.16 [JsonPropertyName("callHierarchyProvider")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public SumType? CallHierarchyProvider { get; init; } + public SumType? CallHierarchyProvider { get; set; } /// /// Gets or sets the value which indicates if semantic tokens is supported. diff --git a/src/LanguageServer/Protocol/RoslynLanguageServer.cs b/src/LanguageServer/Protocol/RoslynLanguageServer.cs index 693e49a7335c..b05eedcc5f1f 100644 --- a/src/LanguageServer/Protocol/RoslynLanguageServer.cs +++ b/src/LanguageServer/Protocol/RoslynLanguageServer.cs @@ -6,6 +6,7 @@ using System.Collections.Frozen; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Threading; @@ -21,6 +22,10 @@ namespace Microsoft.CodeAnalysis.LanguageServer; internal sealed class RoslynLanguageServer : SystemTextJsonLanguageServer, IOnInitialized { + private static int s_clientProcessId = -1; + private static readonly Lazy s_currentProcessId = new(static () => { using var process = Process.GetCurrentProcess(); return process.Id; }); + public static int ServerProcessId => s_currentProcessId.Value; + private readonly AbstractLspServiceProvider _lspServiceProvider; private readonly FrozenDictionary> _baseServices; private readonly WellKnownLspServerKinds _serverKind; @@ -46,6 +51,45 @@ public RoslynLanguageServer( Initialize(); } + public static bool TryRegisterClientProcessId(int clientProcessId) + { + if (s_clientProcessId != -1) + return false; + + if (clientProcessId == ServerProcessId) + return false; + + if (Interlocked.CompareExchange(ref s_clientProcessId, clientProcessId, -1) != -1) + return false; + + _ = WaitForClientProcessExitAsync(s_clientProcessId); + return true; + + static async Task WaitForClientProcessExitAsync(int clientProcessId) + { + try + { + var clientProcessExitTask = new TaskCompletionSource(); + + using var clientProcess = Process.GetProcessById(clientProcessId); + clientProcess.EnableRaisingEvents = true; + clientProcess.Exited += (sender, args) => clientProcessExitTask.SetResult(true); + + if (!clientProcess.HasExited) + { + // Wait for the client process to exit. + await clientProcessExitTask.Task.ConfigureAwait(false); + } + } + finally + { + // The process didn't exist, exited, or we ran into + // issues checking whether the process had exited. + Environment.Exit(ServerExitCodes.ClientProcessExited); + } + } + } + public static SystemTextJsonFormatter CreateJsonMessageFormatter() { var messageFormatter = new SystemTextJsonFormatter(); @@ -215,7 +259,7 @@ public override bool TryGetLanguageForRequest(string methodName, object? seriali if (!lspWorkspaceManager.TryGetLanguageForUri(uri, out language)) { - Logger.LogError($"Failed to get language for {uri} with language {language}"); + Logger.LogDebug($"Failed to get language for {uri} with language {language}"); return false; } diff --git a/src/LanguageServer/Protocol/ServerExitCodes.cs b/src/LanguageServer/Protocol/ServerExitCodes.cs new file mode 100644 index 000000000000..27758594d43b --- /dev/null +++ b/src/LanguageServer/Protocol/ServerExitCodes.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.LanguageServer; + +/// +/// Exit codes used by the language server to indicate different termination reasons. +/// +internal static class ServerExitCodes +{ + /// + /// The server is exiting because the client process has exited. + /// This occurs when the server is monitoring a client process (via the processId + /// provided in the initialize request) and that process exits, or when we encounter + /// issues while trying to determine if the client process is still running. + /// + public const int ClientProcessExited = 1; +} diff --git a/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs b/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs index 0d92f032e1ea..83a8a9854046 100644 --- a/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs +++ b/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; @@ -20,22 +21,25 @@ namespace Microsoft.CodeAnalysis.LanguageServer; internal interface ILspMiscellaneousFilesWorkspaceProvider : ILspService { /// - /// Returns whether the document is one that came from a previous call to . - /// - ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument document, CancellationToken cancellationToken); - - /// - /// Adds a document to the workspace. Note that the implementation of this method should not depend on anything expensive such as RPC calls. + /// Adds the document to an appropriate workspace. May initiate work to load a project for the document. + /// Note that the implementation of this method should not depend on anything expensive such as RPC calls. /// async is used here to allow taking locks asynchronously and "relatively fast" stuff like that. /// - ValueTask AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger); + ValueTask AddDocumentAsync(DocumentUri documentUri, TrackedDocumentInfo trackedDocumentInfo); /// - /// Removes the document with the given from the workspace. - /// If the workspace already does not contain such a document, does nothing. - /// Note that the implementation of this method should not depend on anything expensive such as RPC calls. - /// async is used here to allow taking locks asynchronously and "relatively fast" stuff like that. + /// Removes the document with the given from the miscellaneous files workspace. + /// Used to remove unneeded documents from the miscellaneous files workspace, + /// when a document is found in a non-miscellaneous files workspace. /// /// when a document was found and removed ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri); + + /// + /// Signals to this provider that the document with the given was closed. + /// Separate from 'TryRemoveMiscellaneousDocumentAsync' because it can either remove documents from the miscellaneous files or host workspace. + /// For example, for file-based apps, we wouldn't want to unload them just because we found a document in a non-miscellaneous files workspace, + /// but we may want to unload the file-based app if its entry point file is closed. + /// + ValueTask CloseDocumentAsync(DocumentUri uri); } diff --git a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs index 5342371a4db8..cae1d3300bbe 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs @@ -8,11 +8,12 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Features.Workspaces; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.CommonLanguageServerProtocol.Framework; using Roslyn.LanguageServer.Protocol; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer; @@ -25,27 +26,19 @@ namespace Microsoft.CodeAnalysis.LanguageServer; /// Future work for this workspace includes supporting basic metadata references (mscorlib, System dlls, etc), /// but that is dependent on having a x-plat mechanism for retrieving those references from the framework / sdk. /// -internal sealed class LspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices) +internal sealed class LspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices, IGlobalOptionService globalOptionService) : Workspace(hostServices, WorkspaceKind.MiscellaneousFiles), ILspMiscellaneousFilesWorkspaceProvider, ILspWorkspace { public bool SupportsMutation => true; - public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument document, CancellationToken cancellationToken) + private readonly ILspLogger _logger = lspServices.GetRequiredService(); + + public async ValueTask AddDocumentAsync(DocumentUri documentUri, TrackedDocumentInfo trackedDocumentInfo) { - // In this case, the only documents ever created live in the Miscellaneous Files workspace (which is this object directly), so we can just compare to 'this'. - return document.Project.Solution.Workspace == this; + return AddMiscellaneousDocument(documentUri, trackedDocumentInfo.SourceText, trackedDocumentInfo.LanguageId); } - /// - /// Takes in a file URI and text and creates a misc project and document for the file. - /// - /// Calls to this method and are made - /// from LSP text sync request handling which do not run concurrently. - /// - public async ValueTask AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) - => AddMiscellaneousDocument(uri, documentText, languageId, logger); - - private TextDocument? AddMiscellaneousDocument(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) + private TextDocument? AddMiscellaneousDocument(DocumentUri uri, SourceText documentText, string languageId) { var documentFilePath = uri.UriString; if (uri.ParsedUri is not null) @@ -57,14 +50,15 @@ public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument docu if (!languageInfoProvider.TryGetLanguageInformation(uri, languageId, out var languageInformation)) { // Only log here since throwing here could take down the LSP server. - logger.LogError($"Could not find language information for {uri} with absolute path {documentFilePath}"); + _logger.LogError($"Could not find language information for {uri} with absolute path {documentFilePath}"); return null; } var sourceTextLoader = new SourceTextLoader(documentText, documentFilePath); + var enableFileBasedPrograms = globalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms); var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( - this, documentFilePath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, Services.SolutionServices, []); + this, documentFilePath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, Services.SolutionServices, [], enableFileBasedPrograms); OnProjectAdded(projectInfo); if (languageInformation.LanguageName == "Razor") @@ -80,7 +74,7 @@ public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument docu /// /// Removes a document with the matching file path from this workspace. /// - /// Calls to this method and are made + /// Calls to this method and are made /// from LSP text sync request handling which do not run concurrently. /// public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri) @@ -109,6 +103,9 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri return false; } + public async ValueTask CloseDocumentAsync(DocumentUri uri) + => await TryRemoveMiscellaneousDocumentAsync(uri).ConfigureAwait(false); + public async ValueTask UpdateTextIfPresentAsync(DocumentId documentId, SourceText sourceText, CancellationToken cancellationToken) { this.OnDocumentTextChanged(documentId, sourceText, PreservationMode.PreserveIdentity, requireDocumentPresent: false); diff --git a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProviderFactory.cs b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProviderFactory.cs index 7a5c6fe44966..c7f7979b67fa 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProviderFactory.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProviderFactory.cs @@ -7,7 +7,7 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer.Handler; -using Microsoft.CodeAnalysis.MetadataAsSource; +using Microsoft.CodeAnalysis.Options; using Microsoft.CommonLanguageServerProtocol.Framework; namespace Microsoft.CodeAnalysis.LanguageServer; @@ -20,10 +20,10 @@ namespace Microsoft.CodeAnalysis.LanguageServer; [ExportCSharpVisualBasicStatelessLspService(typeof(ILspMiscellaneousFilesWorkspaceProviderFactory)), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class LspMiscellaneousFilesWorkspaceProviderFactory() : ILspMiscellaneousFilesWorkspaceProviderFactory +internal sealed class LspMiscellaneousFilesWorkspaceProviderFactory(IGlobalOptionService globalOptionService) : ILspMiscellaneousFilesWorkspaceProviderFactory { public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices) { - return new LspMiscellaneousFilesWorkspaceProvider(lspServices, hostServices); + return new LspMiscellaneousFilesWorkspaceProvider(lspServices, hostServices, globalOptionService); } } diff --git a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs index fdc084c71bdb..34f09639d07a 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs @@ -157,7 +157,7 @@ public async ValueTask StopTrackingAsync(DocumentUri uri, CancellationToken canc { try { - await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri).ConfigureAwait(false); + await _lspMiscellaneousFilesWorkspaceProvider.CloseDocumentAsync(uri).ConfigureAwait(false); } catch (Exception ex) when (FatalError.ReportAndCatch(ex)) { @@ -257,29 +257,11 @@ public void UpdateTrackedDocument(DocumentUri uri, SourceText newSourceText, int // We have at least one document, so find the one in the right project context. var document = documents.FindDocumentInProjectContext(textDocumentIdentifier, (sln, id) => sln.GetRequiredTextDocument(id)); - if (_lspMiscellaneousFilesWorkspaceProvider is not null) + if (workspace.Kind != WorkspaceKind.MiscellaneousFiles && _lspMiscellaneousFilesWorkspaceProvider is not null) { - // It is possible that a document that was previously a misc file is now part of a real workspace (e.g. project system told us about a file we already had open). - // If we found a non-misc document, we should clean up any references to it in the misc provider. - var foundNonMiscDocument = await documents - .AnyAsync(async doc => !await _lspMiscellaneousFilesWorkspaceProvider.IsMiscellaneousFilesDocumentAsync(doc, cancellationToken).ConfigureAwait(false)) - .ConfigureAwait(false); - if (foundNonMiscDocument) - { - try - { - var didRemove = await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri).ConfigureAwait(false); - if (didRemove) - { - // If we actually removed something, lookup the document again to ensure we return updated solutions without the misc document. - return await GetLspDocumentInfoAsync(textDocumentIdentifier, cancellationToken).ConfigureAwait(false); - } - } - catch (Exception ex) when (FatalError.ReportAndCatch(ex)) - { - _logger.LogException(ex); - } - } + // Found the document in a non-miscellaneous files workspace. + // Unload it from the miscellaneous files workspace. + await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri).ConfigureAwait(false); } // Record metadata on how we got this document. @@ -303,7 +285,7 @@ public void UpdateTrackedDocument(DocumentUri uri, SourceText newSourceText, int { try { - var miscDocument = await _lspMiscellaneousFilesWorkspaceProvider.AddMiscellaneousDocumentAsync(uri, trackedDocument.SourceText, trackedDocument.LanguageId, _logger).ConfigureAwait(false); + var miscDocument = await _lspMiscellaneousFilesWorkspaceProvider.AddDocumentAsync(uri, trackedDocument).ConfigureAwait(false); if (miscDocument is not null) return (miscDocument.Project.Solution.Workspace, miscDocument.Project.Solution, miscDocument); } @@ -587,7 +569,7 @@ public TestAccessor(LspWorkspaceManager manager) public ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument document) { - return _manager._lspMiscellaneousFilesWorkspaceProvider!.IsMiscellaneousFilesDocumentAsync(document, CancellationToken.None); + return ValueTask.FromResult(document.Project.Solution.WorkspaceKind == WorkspaceKind.MiscellaneousFiles); } public async IAsyncEnumerable GetMiscellaneousDocumentsAsync(Func> documentSelector) where T : TextDocument diff --git a/src/LanguageServer/ProtocolUnitTests/CallHierarchy/CallHierarchyTests.cs b/src/LanguageServer/ProtocolUnitTests/CallHierarchy/CallHierarchyTests.cs new file mode 100644 index 000000000000..f4fef25173a3 --- /dev/null +++ b/src/LanguageServer/ProtocolUnitTests/CallHierarchy/CallHierarchyTests.cs @@ -0,0 +1,206 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.CallHierarchy; + +public sealed class CallHierarchyTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) +{ + [Theory, CombinatorialData] + public async Task TestPrepareCallHierarchyIncludesContainingTypeInName(bool mutatingLspWorkspace) + { + var markup = """ + class C + { + void {|definition:M|}() + { + } + + void N() + { + {|caret:|}M(); + } + } + """; + + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + + var preparedItems = await RunPrepareCallHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + + var preparedItem = Assert.Single(preparedItems); + var definition = testLspServer.GetLocations("definition").Single(); + + Assert.Equal("C.M()", preparedItem.Name); + Assert.Equal(definition.DocumentUri, preparedItem.Uri); + Assert.Equal(0, CompareRange(definition.Range, preparedItem.SelectionRange)); + Assert.NotNull(preparedItem.Data); + } + + [Theory, CombinatorialData] + public async Task TestOutgoingCallsIncludeImplicitConstructors(bool mutatingLspWorkspace) + { + var markup = """ + class {|type:C|} + { + public void {|targetMethod:M|}() + { + } + } + + class Caller + { + void {|caret:N|}() + { + var c = new C(); + c.M(); + } + } + """; + + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + + var preparedItem = Assert.Single(await RunPrepareCallHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single())); + var outgoingCalls = await RunOutgoingCallsAsync(testLspServer, preparedItem); + + Assert.Equal(2, outgoingCalls.Length); + + var constructorCall = Assert.Single(outgoingCalls, static call => call.To.Name == "C.C()"); + var methodCall = Assert.Single(outgoingCalls, static call => call.To.Name == "C.M()"); + + var typeLocation = testLspServer.GetLocations("type").Single(); + var methodLocation = testLspServer.GetLocations("targetMethod").Single(); + + Assert.Equal(typeLocation.DocumentUri, constructorCall.To.Uri); + Assert.Equal(0, CompareRange(typeLocation.Range, constructorCall.To.SelectionRange)); + Assert.Single(constructorCall.FromRanges); + + Assert.Equal(methodLocation.DocumentUri, methodCall.To.Uri); + Assert.Equal(0, CompareRange(methodLocation.Range, methodCall.To.SelectionRange)); + Assert.Single(methodCall.FromRanges); + } + + [Theory, CombinatorialData] + public async Task TestIncomingCallsIncludesManyCallers(bool mutatingLspWorkspace) + { + var markup = """ + class Target + { + void {|definition:M|}() + { + } + } + + class Caller + { + void {|caller1:Caller1|}() + { + var target = new Target(); + target.{|caret:|}M(); + } + + void {|caller2:Caller2|}() + { + var target = new Target(); + target.M(); + } + } + """; + + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + + var preparedItem = Assert.Single(await RunPrepareCallHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single())); + var incomingCalls = await RunIncomingCallsAsync(testLspServer, preparedItem); + + Assert.Equal(2, incomingCalls.Length); + + var firstCaller = Assert.Single(incomingCalls, static call => call.From.Name == "Caller.Caller1()"); + var secondCaller = Assert.Single(incomingCalls, static call => call.From.Name == "Caller.Caller2()"); + + var firstCallerLocation = testLspServer.GetLocations("caller1").Single(); + var secondCallerLocation = testLspServer.GetLocations("caller2").Single(); + + Assert.Equal(0, CompareRange(firstCallerLocation.Range, firstCaller.From.SelectionRange)); + Assert.Equal(0, CompareRange(secondCallerLocation.Range, secondCaller.From.SelectionRange)); + Assert.Single(firstCaller.FromRanges); + Assert.Single(secondCaller.FromRanges); + } + + [Theory, CombinatorialData] + public async Task TestOutgoingCallsIncludesMethodPropertyAndField(bool mutatingLspWorkspace) + { + var markup = """ + class C + { + int {|field:F|}; + + int {|property:P|} => F; + + void {|method:M|}() + { + } + + void {|caret:N|}() + { + M(); + var p = P; + var f = F; + } + } + """; + + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + + var preparedItem = Assert.Single(await RunPrepareCallHierarchyAsync(testLspServer, testLspServer.GetLocations("caret").Single())); + var outgoingCalls = await RunOutgoingCallsAsync(testLspServer, preparedItem); + + Assert.Equal(3, outgoingCalls.Length); + + var methodCall = Assert.Single(outgoingCalls, static call => call.To.Name == "C.M()"); + var propertyCall = Assert.Single(outgoingCalls, static call => call.To.Name == "C.P"); + var fieldCall = Assert.Single(outgoingCalls, static call => call.To.Name == "C.F"); + + Assert.Equal(0, CompareRange(testLspServer.GetLocations("method").Single().Range, methodCall.To.SelectionRange)); + Assert.Equal(0, CompareRange(testLspServer.GetLocations("property").Single().Range, propertyCall.To.SelectionRange)); + Assert.Equal(0, CompareRange(testLspServer.GetLocations("field").Single().Range, fieldCall.To.SelectionRange)); + + Assert.Single(methodCall.FromRanges); + Assert.Single(propertyCall.FromRanges); + Assert.Single(fieldCall.FromRanges); + } + + private static async Task RunPrepareCallHierarchyAsync(TestLspServer testLspServer, LSP.Location caret) + => await testLspServer.ExecuteRequestAsync( + LSP.Methods.PrepareCallHierarchyName, + new LSP.CallHierarchyPrepareParams + { + TextDocument = CreateTextDocumentIdentifier(caret.DocumentUri), + Position = caret.Range.Start, + }, + CancellationToken.None) ?? []; + + private static async Task RunOutgoingCallsAsync(TestLspServer testLspServer, LSP.CallHierarchyItem item) + => await testLspServer.ExecuteRequestAsync( + LSP.Methods.CallHierarchyOutgoingCallsName, + new LSP.CallHierarchyOutgoingCallsParams + { + Item = item, + }, + CancellationToken.None) ?? []; + + private static async Task RunIncomingCallsAsync(TestLspServer testLspServer, LSP.CallHierarchyItem item) + => await testLspServer.ExecuteRequestAsync( + LSP.Methods.CallHierarchyIncomingCallsName, + new LSP.CallHierarchyIncomingCallsParams + { + Item = item, + }, + CancellationToken.None) ?? []; +} diff --git a/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs b/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs index b6ec3a06af87..0c1edebec2b0 100644 --- a/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs @@ -731,8 +731,7 @@ public partial class C Assert.Null(results.Single().Diagnostics); Assert.Equal(firstResultId, secondResultId); - testLspServer.TestWorkspace.EnqueueUpdateSourceGeneratorVersion(document.Project.Id, forceRegeneration: false); - await testLspServer.WaitForSourceGeneratorsAsync(); + await testLspServer.RefreshSourceGeneratorsAsync(forceRegeneration: false); results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics, previousResultId: secondResultId); var thirdResultId = results.Single().ResultId; diff --git a/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.cs b/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.cs index 76678098035d..225ef744bbe7 100644 --- a/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/DocumentChanges/DocumentChangesTests.cs @@ -104,13 +104,17 @@ void M() } """, mutatingLspWorkspace); - await using (testLspServer) + try { await DidOpen(testLspServer, locationTyped.DocumentUri); await Assert.ThrowsAnyAsync(() => DidOpen(testLspServer, locationTyped.DocumentUri)); await testLspServer.AssertServerShuttingDownAsync(); } + finally + { + await Assert.ThrowsAsync(async () => await testLspServer.DisposeAsync()); + } } [Theory, CombinatorialData] @@ -126,11 +130,15 @@ void M() } """, mutatingLspWorkspace); - await using (testLspServer) + try { await Assert.ThrowsAnyAsync(() => DidClose(testLspServer, locationTyped.DocumentUri)); await testLspServer.AssertServerShuttingDownAsync(); } + finally + { + await Assert.ThrowsAsync(async () => await testLspServer.DisposeAsync()); + } } [Theory, CombinatorialData] @@ -146,11 +154,15 @@ void M() } """, mutatingLspWorkspace); - await using (testLspServer) + try { await Assert.ThrowsAnyAsync(() => DidChange(testLspServer, locationTyped.DocumentUri, (0, 0, "goo"))); await testLspServer.AssertServerShuttingDownAsync(); } + finally + { + await Assert.ThrowsAsync(async () => await testLspServer.DisposeAsync()); + } } [Theory, CombinatorialData] diff --git a/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs b/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs index 3ec74706852c..f32bf9dbf70d 100644 --- a/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs @@ -4,6 +4,7 @@ using System; using System.Composition; +using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -127,11 +128,18 @@ public async Task ThrowsIfDeserializationFails(bool mutatingLspWorkspace) [Theory, CombinatorialData] public async Task ShutsdownIfDeserializationFailsOnMutatingRequest(bool mutatingLspWorkspace) { - await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace); + var server = await CreateTestLspServerAsync("", mutatingLspWorkspace); - var request = new TestRequestTypeThree("value"); - await Assert.ThrowsAnyAsync(async () => await server.ExecuteRequestAsync(TestDocumentHandler.MethodName, request, CancellationToken.None)); - await server.AssertServerShuttingDownAsync(); + try + { + var request = new TestRequestTypeThree("value"); + await Assert.ThrowsAnyAsync(async () => await server.ExecuteRequestAsync(TestDocumentHandler.MethodName, request, CancellationToken.None)); + await server.AssertServerShuttingDownAsync(); + } + finally + { + await Assert.ThrowsAsync(async () => await server.DisposeAsync()); + } } [Theory, CombinatorialData] @@ -279,19 +287,42 @@ await Assert.ThrowsAnyAsync(async () [Theory, CombinatorialData] public async Task TestMutatingHandlerCrashesIfUnableToDetermineLanguage(bool mutatingLspWorkspace) { - await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + try + { + // Run a mutating request against a file which we have no saved languageId for + // and where the language cannot be determined from the URI. + // This should crash the server. + var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"untitled:untitledFile"); + var request = new TestRequestTypeOne(new TextDocumentIdentifier + { + DocumentUri = looseFileUri + }); + + await Assert.ThrowsAnyAsync(async () => await testLspServer.ExecuteRequestAsync(TestDocumentHandler.MethodName, request, CancellationToken.None)).ConfigureAwait(false); + await testLspServer.AssertServerShuttingDownAsync(); + } + finally + { + await Assert.ThrowsAsync(async () => await testLspServer.DisposeAsync()); + } + } + + [Theory, CombinatorialData] + public async Task DoesNotCrashOnRequestForMissingHandler(bool mutatingLspWorkspace) + { + await using var server = await CreateTestLspServerAsync("", mutatingLspWorkspace); - // Run a mutating request against a file which we have no saved languageId for - // and where the language cannot be determined from the URI. - // This should crash the server. - var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"untitled:untitledFile"); var request = new TestRequestTypeOne(new TextDocumentIdentifier { - DocumentUri = looseFileUri + DocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\test.cs") }); - await Assert.ThrowsAnyAsync(async () => await testLspServer.ExecuteRequestAsync(TestDocumentHandler.MethodName, request, CancellationToken.None)).ConfigureAwait(false); - await testLspServer.AssertServerShuttingDownAsync(); + await Assert.ThrowsAnyAsync(async () + => await server.ExecuteRequestAsync("nonExistentMethod", request, CancellationToken.None)); + Assert.False(server.GetServerAccessor().HasShutdownStarted()); + Assert.False(server.GetQueueAccessor()!.Value.IsComplete()); } internal sealed record TestRequestTypeOne([property: JsonPropertyName("textDocument"), JsonRequired] TextDocumentIdentifier TextDocumentIdentifier); diff --git a/src/LanguageServer/ProtocolUnitTests/Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj b/src/LanguageServer/ProtocolUnitTests/Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj index 77dcec96afa1..4ea1628fa830 100644 --- a/src/LanguageServer/ProtocolUnitTests/Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj +++ b/src/LanguageServer/ProtocolUnitTests/Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj @@ -3,7 +3,7 @@ - $(NetVSCode);net472 + $(NetRoslynWindowsTests);net472 Library Microsoft.CodeAnalysis.LanguageServer.UnitTests UnitTest @@ -23,7 +23,6 @@ - diff --git a/src/LanguageServer/ProtocolUnitTests/Ordering/RequestOrderingTests.cs b/src/LanguageServer/ProtocolUnitTests/Ordering/RequestOrderingTests.cs index 0eebd77b0a51..b0930e69b231 100644 --- a/src/LanguageServer/ProtocolUnitTests/Ordering/RequestOrderingTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Ordering/RequestOrderingTests.cs @@ -168,19 +168,26 @@ public async Task FailingMutableTaskShutsDownQueue(bool mutatingLspWorkspace) new TestRequest(NonMutatingRequestHandler.MethodName), }; - await using var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace); - var waitables = StartTestRun(testLspServer, requests); + var testLspServer = await CreateTestLspServerAsync("class C { }", mutatingLspWorkspace); + try + { + var waitables = StartTestRun(testLspServer, requests); - // first task should fail - await Assert.ThrowsAsync(() => waitables[0]); + // first task should fail + await Assert.ThrowsAsync(() => waitables[0]); - // The failed request returns to the client before the shutdown completes. - // Wait for the queue to finish handling the failed request and shutdown. - await testLspServer.GetQueueAccessor()!.Value.WaitForProcessingToStopAsync().ConfigureAwait(false); + // The failed request returns to the client before the shutdown completes. + // Wait for the queue to finish handling the failed request and shutdown. + await testLspServer.GetQueueAccessor()!.Value.WaitForProcessingToStopAsync().ConfigureAwait(false); - // remaining tasks should be canceled - var areAllItemsCancelled = await testLspServer.GetQueueAccessor()!.Value.AreAllItemsCancelledUnsafeAsync(); - Assert.True(areAllItemsCancelled); + // remaining tasks should be canceled + var areAllItemsCancelled = await testLspServer.GetQueueAccessor()!.Value.AreAllItemsCancelledUnsafeAsync(); + Assert.True(areAllItemsCancelled); + } + finally + { + await Assert.ThrowsAsync(async () => await testLspServer.DisposeAsync()); + } } [Theory, CombinatorialData] diff --git a/src/LanguageServer/ProtocolUnitTests/References/FindImplementationsTests.cs b/src/LanguageServer/ProtocolUnitTests/References/FindImplementationsTests.cs index e72b7d5afa3c..ed58c2a75c7e 100644 --- a/src/LanguageServer/ProtocolUnitTests/References/FindImplementationsTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/References/FindImplementationsTests.cs @@ -31,7 +31,7 @@ interface IA } class A : IA { - void IA.{|implementation:M|}() + void IA.{|implementation:|}M() { } } @@ -61,7 +61,7 @@ namespace One { class A : IA { - void IA.{|implementation:M|}() + void IA.{|implementation:|}M() { } } @@ -128,9 +128,9 @@ public async Task TestFindImplementationAsync_MultipleLocations(bool mutatingLsp """ class {|caret:|}A { } - class {|implementation:B|} : A { } + class {|implementation:|}B : A { } - class {|implementation:C|} : A { } + class {|implementation:|}C : A { } """; await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); @@ -145,7 +145,7 @@ public async Task TestFindImplementationAsync_NoMetadataResults(bool mutatingLsp using System; class C : IDisposable { - public void {|implementation:Dispose|}() + public void {|implementation:|}Dispose() { IDisposable d; d.{|caret:|}Dispose(); diff --git a/src/LanguageServer/ProtocolUnitTests/SelectionRanges/SelectionRangesTests.cs b/src/LanguageServer/ProtocolUnitTests/SelectionRanges/SelectionRangesTests.cs new file mode 100644 index 000000000000..9a9a5e440d16 --- /dev/null +++ b/src/LanguageServer/ProtocolUnitTests/SelectionRanges/SelectionRangesTests.cs @@ -0,0 +1,255 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.SelectionRanges; + +public sealed class SelectionRangesTests(ITestOutputHelper testOutputHelper) + : AbstractLanguageServerProtocolTests(testOutputHelper) +{ + /// + /// Caret at the literal 1 inside a binary expression in a method body. + /// The chain expands through literal → binary → equals-value → declarator → + /// declaration → local-decl-stmt → block → method → class. + /// + [Theory, CombinatorialData] + public Task TestGetSelectionRangeAsync_MethodBody(bool mutatingLspWorkspace) + => AssertSelectionRangesAsync(mutatingLspWorkspace, + """ + [|class C + { + [|void M() + [|{ + [|[|var [|x [|= [|[|{|caret:|}1|] + 2|]|]|]|];|] + }|]|] + }|] + """); + + /// + /// Caret at 1 in a top-level statement file (no class or namespace wrapper). + /// The chain reaches the compilation unit directly. + /// + [Theory, CombinatorialData] + public Task TestGetSelectionRangeAsync_TopLevelStatements(bool mutatingLspWorkspace) + => AssertSelectionRangesAsync(mutatingLspWorkspace, + """ + [|[|[|var [|x [|= [|[|{|caret:|}1|] + 2|]|]|]|];|] + var y = 1;|] + """); + + /// + /// Caret at 1 in a method inside a file-scoped namespace. + /// The outermost range is the FileScopedNamespaceDeclarationSyntax. + /// + [Theory, CombinatorialData] + public Task TestGetSelectionRangeAsync_FileScopedNamespace(bool mutatingLspWorkspace) + => AssertSelectionRangesAsync(mutatingLspWorkspace, + """ + [|namespace MyNamespace; + [|class C + { + [|void M() + [|{ + [|[|var [|x [|= [|{|caret:|}1|]|]|]|];|] + }|]|] + }|]|] + """); + + /// + /// Caret at 1 inside a doubly-nested namespace. + /// The chain expands through both inner and outer NamespaceDeclarationSyntax nodes. + /// + [Theory, CombinatorialData] + public Task TestGetSelectionRangeAsync_NestedNamespaces(bool mutatingLspWorkspace) + => AssertSelectionRangesAsync(mutatingLspWorkspace, + """ + [|namespace Outer + { + [|namespace Inner + { + [|class C + { + [|void M() + [|{ + [|[|var [|x [|= [|{|caret:|}1|]|]|]|];|] + }|]|] + }|] + }|] + }|] + """); + + /// + /// Caret at a inside the body of a local function. + /// The chain expands through the local function's block and then the outer method's block. + /// + [Theory, CombinatorialData] + public Task TestGetSelectionRangeAsync_LocalFunction(bool mutatingLspWorkspace) + => AssertSelectionRangesAsync(mutatingLspWorkspace, + """ + [|class C + { + [|void M() + [|{ + [|int Compute(int a, int b) + [|{ + [|return [|[|{|caret:|}a|] + b|];|] + }|]|] + _ = Compute(1, 2); + }|]|] + }|] + """); + + /// + /// Caret at a inside an expression-bodied method. + /// The chain expands through binary → arrow-expression-clause → method → class. + /// + [Theory, CombinatorialData] + public Task TestGetSelectionRangeAsync_ExpressionBodyMember(bool mutatingLspWorkspace) + => AssertSelectionRangesAsync(mutatingLspWorkspace, + """ + [|class C + { + [|int Compute(int a, int b) [|=> [|[|{|caret:|}a|] + b|]|];|] + }|] + """); + + /// + /// Caret at 1 (the true-branch literal) inside a conditional expression. + /// The chain expands through the full ternary before reaching the enclosing declaration. + /// + [Theory, CombinatorialData] + public Task TestGetSelectionRangeAsync_ConditionalExpression(bool mutatingLspWorkspace) + => AssertSelectionRangesAsync(mutatingLspWorkspace, + """ + [|class C + { + [|void M(bool flag) + [|{ + [|[|var [|x [|= [|flag ? [|{|caret:|}1|] : 2|]|]|]|];|] + }|]|] + }|] + """); + + /// + /// Caret at the return keyword inside a single-line if statement. + /// The chain expands: return-stmt → if-stmt → block → method → class. + /// + [Theory, CombinatorialData] + public Task TestGetSelectionRangeAsync_SingleLineIf(bool mutatingLspWorkspace) + => AssertSelectionRangesAsync(mutatingLspWorkspace, + """ + [|class C + { + [|void M() + [|{ + [|if (true) [|{|caret:|}return;|]|] + }|]|] + }|] + """); + + [Theory, CombinatorialData] + public async Task TestGetSelectionRangeAsync_MultiplePositions(bool mutatingLspWorkspace) + { + var markup = + """ + class C + { + void M() + { + var x = {|caret1:|}1; + var y = {|caret2:|}2; + } + } + """; + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + var caret1 = testLspServer.GetLocations("caret1").Single(); + var caret2 = testLspServer.GetLocations("caret2").Single(); + + var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First(); + var request = new LSP.SelectionRangeParams + { + TextDocument = CreateTextDocumentIdentifier(document.GetURI()), + Positions = [caret1.Range.Start, caret2.Range.Start] + }; + + var results = await testLspServer.ExecuteRequestAsync( + LSP.Methods.TextDocumentSelectionRangeName, request, CancellationToken.None); + + Assert.NotNull(results); + Assert.Equal(2, results.Length); + AssertRangeChainIsNestedCorrectly(results[0]); + AssertRangeChainIsNestedCorrectly(results[1]); + } + + private async Task AssertSelectionRangesAsync(bool mutatingLspWorkspace, string markup) + { + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); + var caret = testLspServer.GetLocations("caret").Single(); + var result = await RunGetSelectionRangeAsync(testLspServer, caret); + Assert.NotNull(result); + + // Collect the actual chain from innermost to outermost. + var chain = new List(); + for (var current = result; current is not null; current = current.Parent) + chain.Add(current.Range); + + // [|...|] spans use the same LIFO stack as named spans, so SelectedSpans is + // returned innermost-first — matching the handler chain order. If the markup + // parser's ordering ever changes, these tests would surface the mismatch as + // a sequence-equality failure. + var testDocument = testLspServer.TestWorkspace.Documents.Single(); + var document = testLspServer.GetCurrentSolution().GetDocument(testDocument.Id)!; + var text = await document.GetTextAsync(CancellationToken.None); + var expected = testDocument.SelectedSpans + .Select(span => ProtocolConversions.TextSpanToRange(span, text)) + .ToList(); + + Assert.Equal(expected, chain); + } + + private static async Task RunGetSelectionRangeAsync(TestLspServer testLspServer, LSP.Location caret) + { + var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First(); + var request = new LSP.SelectionRangeParams + { + TextDocument = CreateTextDocumentIdentifier(document.GetURI()), + Positions = [caret.Range.Start] + }; + + var results = await testLspServer.ExecuteRequestAsync( + LSP.Methods.TextDocumentSelectionRangeName, request, CancellationToken.None); + + return results?.FirstOrDefault(); + } + + private static void AssertRangeChainIsNestedCorrectly(LSP.SelectionRange selectionRange) + { + var current = selectionRange; + while (current.Parent is not null) + { + Assert.True( + ContainsOrEquals(current.Parent.Range, current.Range), + $"Parent range {current.Parent.Range} should contain child range {current.Range}"); + current = current.Parent; + } + } + + private static bool ContainsOrEquals(LSP.Range outer, LSP.Range inner) + { + var outerStart = (outer.Start.Line, outer.Start.Character); + var outerEnd = (outer.End.Line, outer.End.Character); + var innerStart = (inner.Start.Line, inner.Start.Character); + var innerEnd = (inner.End.Line, inner.End.Character); + + return outerStart.CompareTo(innerStart) <= 0 && outerEnd.CompareTo(innerEnd) >= 0; + } +} diff --git a/src/LanguageServer/ProtocolUnitTests/UriTests.cs b/src/LanguageServer/ProtocolUnitTests/UriTests.cs index 9bf3c82952be..85c693916407 100644 --- a/src/LanguageServer/ProtocolUnitTests/UriTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/UriTests.cs @@ -27,7 +27,7 @@ public UriTests(ITestOutputHelper? testOutputHelper) : base(testOutputHelper) { } - protected override TestComposition Composition => base.Composition.AddParts(typeof(CustomResolveHandler)); + protected override TestComposition Composition => base.Composition.AddParts(typeof(CustomResolveHandler), typeof(LanguageSpecificHandler)); [Theory, CombinatorialData] [WorkItem("https://github.com/dotnet/runtime/issues/89538")] @@ -352,6 +352,50 @@ public async Task TestOpenDocumentWithInvalidUri(bool mutatingLspWorkspace, stri Assert.Equal("hello", (await document!.GetTextAsync()).ToString()); } + [Theory, CombinatorialData] + public async Task TestUnparseableUri_FindsLanguageSpecificHandler_WhenDocumentIsOpen(bool mutatingLspWorkspace) + { + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + // Use a URI that System.Uri cannot parse. + var unparseableUri = new DocumentUri("_claude_vscode_fs_right:/c:/Projects/MyApp/Pages/Component.razor"); + Assert.Null(unparseableUri.ParsedUri); + + // Open the document with the unparseable URI and "razor" language ID. + // The language ID should be saved and used to route subsequent requests to the Razor-specific handler. + await testLspServer.OpenDocumentAsync(unparseableUri, "
hello
", languageId: "razor"); + + // Send a request to the language-specific handler - should route to the Razor handler successfully. + var response = await testLspServer.ExecuteRequestAsync( + LanguageSpecificHandler.MethodName, + new CustomResolveParams(new LSP.TextDocumentIdentifier { DocumentUri = unparseableUri }), + CancellationToken.None); + + Assert.NotNull(response); + Assert.True(response.HandlerCalled); + } + + [Theory, CombinatorialData] + public async Task TestUnparseableUri_WithNoDefaultHandler_DoesNotCrashServer_WhenDocumentIsClosed(bool mutatingLspWorkspace) + { + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + // Use a URI that System.Uri cannot parse. + var unparseableUri = new DocumentUri("_claude_vscode_fs_right:/c:/Projects/MyApp/Pages/Component.razor"); + Assert.Null(unparseableUri.ParsedUri); + + // Send a request to a handler that is ONLY registered for "Razor" language (no default handler). + // Language lookup fails (document closed, URI unparseable) so there is no language to route to. + // The server should gracefully fail the request, not crash. + await Assert.ThrowsAnyAsync(async () + => await testLspServer.ExecuteRequestAsync( + LanguageSpecificHandler.MethodName, + new CustomResolveParams(new LSP.TextDocumentIdentifier { DocumentUri = unparseableUri }), + CancellationToken.None)); + Assert.False(testLspServer.GetServerAccessor().HasShutdownStarted()); + Assert.False(testLspServer.GetQueueAccessor()!.Value.IsComplete()); + } + [Theory] [InlineData(true, null, null)] [InlineData(false, "file://c:\\valid", null)] @@ -371,6 +415,7 @@ public void TestUriEquality(bool areEqual, string? uriString1, string? uriString private sealed record class ResolvedDocumentInfo(string WorkspaceKind, string ProjectLanguage); private sealed record class CustomResolveParams([property: JsonPropertyName("textDocument")] LSP.TextDocumentIdentifier TextDocument); + private sealed record class LanguageSpecificResponse(bool HandlerCalled); [ExportCSharpVisualBasicStatelessLspService(typeof(CustomResolveHandler)), PartNotDiscoverable, Shared] [LanguageServerEndpoint(MethodName, LanguageServerConstants.DefaultLanguageName)] @@ -388,4 +433,24 @@ public async Task HandleRequestAsync(CustomResolveParams r return new ResolvedDocumentInfo(context.Workspace!.Kind!, context.GetRequiredDocument().Project.Language); } } + + /// + /// Test handler that is only registered for the "Razor" language, with no default fallback. + /// This simulates handlers like textDocument/documentColor that are dynamically registered by Razor. + /// + [ExportCSharpVisualBasicStatelessLspService(typeof(LanguageSpecificHandler)), PartNotDiscoverable, Shared] + [LanguageServerEndpoint(MethodName, "Razor")] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class LanguageSpecificHandler() : ILspServiceRequestHandler + { + public const string MethodName = nameof(LanguageSpecificHandler); + + public bool MutatesSolutionState => false; + public bool RequiresLSPSolution => false; + public Task HandleRequestAsync(CustomResolveParams request, RequestContext context, CancellationToken cancellationToken) + { + return Task.FromResult(new LanguageSpecificResponse(true)); + } + } } diff --git a/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs b/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs index ceddfdad77eb..ddd064555352 100644 --- a/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs @@ -308,7 +308,7 @@ public async Task TestLspUpdatesCorrectWorkspaceWithMultipleWorkspacesAsync(bool $""" - FirstWorkspace + FirstWorkspace """; @@ -318,7 +318,7 @@ public async Task TestLspUpdatesCorrectWorkspaceWithMultipleWorkspacesAsync(bool testWorkspaceTwo.InitializeDocuments(XElement.Parse($""" - SecondWorkspace + SecondWorkspace """)); @@ -330,8 +330,8 @@ public async Task TestLspUpdatesCorrectWorkspaceWithMultipleWorkspacesAsync(bool Assert.True(IsWorkspaceRegistered(testLspServer.TestWorkspace, testLspServer)); Assert.True(IsWorkspaceRegistered(testWorkspaceTwo, testLspServer)); - var firstWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\FirstWorkspace.cs"); - var secondWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SecondWorkspace.cs"); + var firstWorkspaceDocumentUri = CreateAbsoluteDocumentUri("FirstWorkspace.cs"); + var secondWorkspaceDocumentUri = CreateAbsoluteDocumentUri("SecondWorkspace.cs"); await testLspServer.OpenDocumentAsync(firstWorkspaceDocumentUri); // Verify we can get both documents from their respective workspaces. @@ -367,7 +367,7 @@ public async Task TestWorkspaceEventUpdatesCorrectWorkspaceWithMultipleWorkspace $""" - FirstWorkspace + FirstWorkspace """; @@ -377,7 +377,7 @@ public async Task TestWorkspaceEventUpdatesCorrectWorkspaceWithMultipleWorkspace testWorkspaceTwo.InitializeDocuments(XElement.Parse($""" - SecondWorkspace + SecondWorkspace """)); @@ -385,8 +385,8 @@ public async Task TestWorkspaceEventUpdatesCorrectWorkspaceWithMultipleWorkspace // Wait for workspace operations to complete for the second workspace. await WaitForWorkspaceOperationsAsync(testWorkspaceTwo); - var firstWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\FirstWorkspace.cs"); - var secondWorkspaceDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SecondWorkspace.cs"); + var firstWorkspaceDocumentUri = CreateAbsoluteDocumentUri("FirstWorkspace.cs"); + var secondWorkspaceDocumentUri = CreateAbsoluteDocumentUri("SecondWorkspace.cs"); await testLspServer.OpenDocumentAsync(firstWorkspaceDocumentUri); // Verify we can get both documents from their respective workspaces. @@ -499,7 +499,7 @@ public async Task TestLspDocumentPreferredOverProjectSystemDocumentAddInMutating [], mutatingLspWorkspace: true, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); // Open the doc - var filePath = "c:\\\ue25b\ud86d\udeac.cs"; + var filePath = TestHelpers.CreateAbsolutePath("\ue25b\ud86d\udeac.cs"); var documentUri = ProtocolConversions.CreateAbsoluteDocumentUri(filePath); await testLspServer.OpenDocumentAsync(documentUri, "Text"); diff --git a/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentTests.cs b/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentTests.cs index 99af09f68126..fe1084a926d1 100644 --- a/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentTests.cs @@ -213,8 +213,7 @@ internal async Task TestReturnsGeneratedSourceWhenManuallyRefreshed(bool mutatin Assert.Equal("// callCount: 0", text.Text); // Updating the execution version should trigger source generators to run in both automatic and balanced mode. - testLspServer.TestWorkspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration: true); - await testLspServer.WaitForSourceGeneratorsAsync(); + await testLspServer.RefreshSourceGeneratorsAsync(forceRegeneration: true); var secondRequest = await testLspServer.ExecuteRequestAsync(SourceGeneratedDocumentGetTextHandler.MethodName, new SourceGeneratorGetTextParams(new LSP.TextDocumentIdentifier { DocumentUri = sourceGeneratorDocumentUri }, ResultId: text.ResultId), CancellationToken.None); @@ -254,10 +253,9 @@ class C var initialSolution = testLspServer.GetCurrentSolution(); var initialExecutionMap = initialSolution.CompilationState.SourceGeneratorExecutionVersionMap.Map; - // Updating the execution version should trigger source generators to run in both automatic and balanced mode. + // Updating the execution version should trigger source generators to run if there are any changes in both automatic and balanced mode. var forceRegeneration = majorVersionUpdate; - testLspServer.TestWorkspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration); - await testLspServer.WaitForSourceGeneratorsAsync(); + await testLspServer.RefreshSourceGeneratorsAsync(forceRegeneration); var solutionWithChangedExecutionVersion = testLspServer.GetCurrentSolution(); @@ -272,6 +270,7 @@ class C } else { + // There are no changes, so source generators won't actually run if we didn't force regeneration Assert.Equal(text.ResultId, secondRequest.ResultId); Assert.Null(secondRequest.Text); } @@ -376,6 +375,66 @@ public async Task TestReturnsNullForRemovedOpenedGeneratedFile(bool mutatingLspW Assert.Null(secondRequest.Text); } + [Theory, CombinatorialData] + internal async Task TestSaveRefreshesSourceGenerators(bool mutatingLspWorkspace, SourceGeneratorExecutionPreference sourceGeneratorExecution) + { + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace); + var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First(); + var documentUri = document.GetURI(); + + var configService = testLspServer.TestWorkspace.ExportProvider.GetExportedValue(); + configService.Options = new WorkspaceConfigurationOptions(SourceGeneratorExecution: sourceGeneratorExecution); + + var callCount = 0; + var generatorReference = await AddGeneratorAsync(new CallbackGenerator(() => ("hintName.cs", "// callCount: " + callCount++)), testLspServer.TestWorkspace); + + var sourceGeneratedDocuments = await testLspServer.GetCurrentSolution().Projects.Single().GetSourceGeneratedDocumentsAsync(); + var sourceGeneratedDocumentIdentity = sourceGeneratedDocuments.Single().Identity; + var sourceGeneratorDocumentUri = SourceGeneratedDocumentUri.Create(sourceGeneratedDocumentIdentity); + + var text = await testLspServer.ExecuteRequestAsync(SourceGeneratedDocumentGetTextHandler.MethodName, + new SourceGeneratorGetTextParams(new LSP.TextDocumentIdentifier { DocumentUri = sourceGeneratorDocumentUri }, ResultId: null), CancellationToken.None); + + AssertEx.NotNull(text); + Assert.Equal("// callCount: 0", text.Text); + + await testLspServer.OpenDocumentAsync(documentUri, string.Empty); + + // Modify a normal document in the workspace. + // In automatic mode this should trigger generators to re-run. + // In balanced mode generators should not re-run. + await testLspServer.TestWorkspace.ChangeDocumentAsync(document.Id, SourceText.From("new text")); + await testLspServer.WaitForSourceGeneratorsAsync(); + + var secondRequest = await testLspServer.ExecuteRequestAsync(SourceGeneratedDocumentGetTextHandler.MethodName, + new SourceGeneratorGetTextParams(new LSP.TextDocumentIdentifier { DocumentUri = sourceGeneratorDocumentUri }, ResultId: text.ResultId), CancellationToken.None); + AssertEx.NotNull(secondRequest); + if (sourceGeneratorExecution == SourceGeneratorExecutionPreference.Automatic) + { + Assert.NotEqual(text.ResultId, secondRequest.ResultId); + Assert.Equal("// callCount: 1", secondRequest.Text); + } + else + { + Assert.Equal(text.ResultId, secondRequest.ResultId); + } + + var didSaveParams = new LSP.DidSaveTextDocumentParams + { + TextDocument = new LSP.TextDocumentIdentifier { DocumentUri = documentUri }, + }; + + // The didSave should now trigger generators to run in balanced mode. In automatic mode it will also trigger but we will already have the updated text. + await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentDidSaveName, didSaveParams, CancellationToken.None); + await testLspServer.WaitForSourceGeneratorsAsync(); + + var thirdRequest = await testLspServer.ExecuteRequestAsync(SourceGeneratedDocumentGetTextHandler.MethodName, + new SourceGeneratorGetTextParams(new LSP.TextDocumentIdentifier { DocumentUri = sourceGeneratorDocumentUri }, ResultId: text.ResultId), CancellationToken.None); + AssertEx.NotNull(thirdRequest); + Assert.NotEqual(secondRequest.ResultId, thirdRequest.ResultId); + Assert.Equal("// callCount: 1", thirdRequest.Text); + } + private async Task CreateTestLspServerWithGeneratorAsync( bool mutatingLspWorkspace, [StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string generatedDocumentText) diff --git a/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj b/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj index a8ed455d0bd2..2eeb7121253a 100644 --- a/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj +++ b/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj @@ -89,7 +89,7 @@ <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Razor.EditorFeatures\$(Configuration)\net472\Microsoft.CodeAnalysis.ExternalAccess.Razor.EditorFeatures.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Razor.Features\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.ExternalAccess.Razor.Features.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Features\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.Features.dll" TargetDir="" /> - <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.InteractiveHost\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.InteractiveHost.dll" TargetDir="" /> + <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.InteractiveHost\$(Configuration)\net472\Microsoft.CodeAnalysis.InteractiveHost.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.LanguageServer.Protocol\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.LanguageServer.Protocol.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Remote.ServiceHub\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.Remote.ServiceHub.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Remote.Workspaces\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.Remote.Workspaces.dll" TargetDir="" /> diff --git a/src/NuGet/VS.Tools.Roslyn.Package/VS.Tools.Roslyn.Package.csproj b/src/NuGet/VS.Tools.Roslyn.Package/VS.Tools.Roslyn.Package.csproj index bfd44a1a0f29..2c35608e8b1f 100644 --- a/src/NuGet/VS.Tools.Roslyn.Package/VS.Tools.Roslyn.Package.csproj +++ b/src/NuGet/VS.Tools.Roslyn.Package/VS.Tools.Roslyn.Package.csproj @@ -45,7 +45,7 @@ - + - $(DefineConstants),ROSLYN_4_12_OR_LOWER + + $(DefineConstants),OLDER_ROSLYN diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/CSharp/MetaAnalyzers/CSharpSymbolIsBannedInAnalyzersAnalyzer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/CSharp/MetaAnalyzers/CSharpSymbolIsBannedInAnalyzersAnalyzer.cs index 3224024b6332..3c0e1ff701af 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/CSharp/MetaAnalyzers/CSharpSymbolIsBannedInAnalyzersAnalyzer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/CSharp/MetaAnalyzers/CSharpSymbolIsBannedInAnalyzersAnalyzer.cs @@ -23,5 +23,8 @@ public sealed class CSharpSymbolIsBannedInAnalyzersAnalyzer : SymbolIsBannedInAn protected override SyntaxNode GetReferenceSyntaxNodeFromXmlCref(SyntaxNode syntaxNode) => ((XmlCrefAttributeSyntax)syntaxNode).Cref; protected override IEnumerable GetTypeSyntaxNodesFromBaseType(SyntaxNode syntaxNode) => ((BaseListSyntax)syntaxNode).Types.Select(t => (SyntaxNode)t.Type); + + protected override bool IsRegularCommentOrDocumentationComment(SyntaxTrivia trivia) + => trivia.Kind() is SyntaxKind.SingleLineCommentTrivia or SyntaxKind.MultiLineCommentTrivia; } } diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Microsoft.CodeAnalysis.Analyzers.sarif b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Microsoft.CodeAnalysis.Analyzers.sarif index 37cc22cad222..689d3c000afd 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Microsoft.CodeAnalysis.Analyzers.sarif +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Microsoft.CodeAnalysis.Analyzers.sarif @@ -5,7 +5,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.Analyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -724,7 +724,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.CSharp.Analyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -957,7 +957,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.VisualBasic.Analyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md index 696057f8206a..449f5da4e537 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md @@ -57,9 +57,9 @@ Consider the following example: ### Changed Rules - Rule ID | Category | Severity | Notes - --------|----------|----------|-------------------- - CA2000 | Security | Disabled | CA2000_AnalyzerName, [Documentation](CA2000_Documentation_Link) + Rule ID | New Category | New Severity | Old Category | Old Severity | Notes + --------|--------------|--------------|--------------|--------------|------- + CA2000 | Security | Disabled | Security | Info | CA2000_AnalyzerName, [Documentation](CA2000_Documentation_Link) ``` 2. `AnalyzerReleases.Unshipped.md`: @@ -76,7 +76,7 @@ Consider the following example: Analyzer author has shipped 2 analyzer releases: 1. Version 1.0 shipped three rules: CA1000, CA2000 and CA3000. -2. Version 2.0 changed the default severity of a shipped rule CA2000 from 'Warning' to 'Disabled'. It removed a shipped rule CA3000 and added a new rule CA4000. +2. Version 2.0 changed the default severity of a shipped rule CA2000 from 'Info' to 'Disabled'. It removed a shipped rule CA3000 and added a new rule CA4000. 3. Upcoming release will add 2 new rules CA5000 and CA6000. When the next release is shipped, say version '3.0', a new release section for 3.0 should be created at the end of the shipped file and the entries from unshipped file should be moved under it. The files will change as below: @@ -111,10 +111,9 @@ When the next release is shipped, say version '3.0', a new release section for 3 ### Changed Rules - Rule ID | Category | Severity | Notes - --------|----------|----------|-------------------- - CA2000 | Security | Disabled | CA2000_AnalyzerName, [Documentation](CA2000_Documentation_Link) - + Rule ID | New Category | New Severity | Old Category | Old Severity | Notes + --------|--------------|--------------|--------------|--------------|------- + CA2000 | Security | Disabled | Security | Info | CA2000_AnalyzerName, [Documentation](CA2000_Documentation_Link) ## Release 3.0 diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/VisualBasic/BasicSymbolIsBannedInAnalyzersAnalyzer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/VisualBasic/BasicSymbolIsBannedInAnalyzersAnalyzer.vb index 10e6384925e7..5ad773750ba1 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/VisualBasic/BasicSymbolIsBannedInAnalyzersAnalyzer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/VisualBasic/BasicSymbolIsBannedInAnalyzersAnalyzer.vb @@ -44,5 +44,9 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Analyzers End If End Function + Protected Overrides Function IsRegularCommentOrDocumentationComment(trivia As SyntaxTrivia) As Boolean + Return trivia.Kind() = SyntaxKind.CommentTrivia OrElse trivia.Kind() = SyntaxKind.DocumentationCommentTrivia + End Function + End Class End Namespace diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md index cf031e98eb1b..b4fc1bb14676 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md @@ -16,6 +16,14 @@ This can be done by: ``` +Generated code is analyzed by default. To exclude generated code from banned API analysis, add the following to an analyzer config file such as `.globalconfig`: + +```ini +is_global = true + +dotnet_banned_api_analyzer.exclude_generated_code = true +``` + To add a symbol to the banned list, just add an entry in the format below to one of the configuration files (Description Text will be displayed as description in diagnostics, which is optional): ```txt diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/CSharp/CSharpSymbolIsBannedAnalyzer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/CSharp/CSharpSymbolIsBannedAnalyzer.cs index 7a8262626fd0..9f55b8e1d8ac 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/CSharp/CSharpSymbolIsBannedAnalyzer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/CSharp/CSharpSymbolIsBannedAnalyzer.cs @@ -20,6 +20,9 @@ public sealed class CSharpSymbolIsBannedAnalyzer : SymbolIsBannedAnalyzer SymbolDisplayFormat.CSharpShortErrorMessageFormat; + protected override bool IsRegularCommentOrDocumentationComment(SyntaxTrivia trivia) + => trivia.Kind() is SyntaxKind.SingleLineCommentTrivia or SyntaxKind.MultiLineCommentTrivia; + protected override SyntaxNode GetReferenceSyntaxNodeFromXmlCref(SyntaxNode syntaxNode) => ((XmlCrefAttributeSyntax)syntaxNode).Cref; protected override IEnumerable GetTypeSyntaxNodesFromBaseType(SyntaxNode syntaxNode) => ((BaseListSyntax)syntaxNode).Types.Select(t => (SyntaxNode)t.Type); diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/Core/SymbolIsBannedAnalyzerBase.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/Core/SymbolIsBannedAnalyzerBase.cs index 8d6e96c742b6..17d7ee8d0705 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/Core/SymbolIsBannedAnalyzerBase.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/Core/SymbolIsBannedAnalyzerBase.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -13,12 +14,15 @@ using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.BannedApiAnalyzers { public abstract class SymbolIsBannedAnalyzerBase : DiagnosticAnalyzer where TSyntaxKind : struct { + private const string ExcludeGeneratedCodeOptionName = "dotnet_banned_api_analyzer.exclude_generated_code"; + protected abstract Dictionary<(string ContainerName, string SymbolName), ImmutableArray>? ReadBannedApis(CompilationStartAnalysisContext compilationContext); protected abstract DiagnosticDescriptor SymbolIsBannedRule { get; } @@ -33,6 +37,8 @@ public abstract class SymbolIsBannedAnalyzerBase : DiagnosticAnalyz protected abstract SymbolDisplayFormat SymbolDisplayFormat { get; } + protected abstract bool IsRegularCommentOrDocumentationComment(SyntaxTrivia trivia); + public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); @@ -49,6 +55,8 @@ private void OnCompilationStart(CompilationStartAnalysisContext compilationConte if (bannedApis == null || bannedApis.Count == 0) return; + var shouldAnalyzeMap = new ConcurrentDictionary(); + if (ShouldAnalyzeAttributes()) { compilationContext.RegisterCompilationEndAction( @@ -59,7 +67,10 @@ private void OnCompilationStart(CompilationStartAnalysisContext compilationConte }); compilationContext.RegisterSymbolAction( - context => VerifyAttributes(context.ReportDiagnostic, context.Symbol.GetAttributes(), context.CancellationToken), + context => + { + VerifyAttributes(context.ReportDiagnostic, context.Symbol.GetAttributes(), context.CancellationToken); + }, SymbolKind.NamedType, SymbolKind.Method, SymbolKind.Field, @@ -71,6 +82,9 @@ private void OnCompilationStart(CompilationStartAnalysisContext compilationConte context => { context.CancellationToken.ThrowIfCancellationRequested(); + if (ShouldSkipOperationAnalysis(context)) + return; + switch (context.Operation) { case IObjectCreationOperation objectCreation: @@ -153,11 +167,23 @@ private void OnCompilationStart(CompilationStartAnalysisContext compilationConte OperationKind.TypeOf); compilationContext.RegisterSyntaxNodeAction( - context => VerifyDocumentationSyntax(context.ReportDiagnostic, GetReferenceSyntaxNodeFromXmlCref(context.Node), context), + context => + { + if (ShouldSkipSyntaxNodeAnalysis(context)) + return; + + VerifyDocumentationSyntax(context.ReportDiagnostic, GetReferenceSyntaxNodeFromXmlCref(context.Node), context); + }, XmlCrefSyntaxKind); compilationContext.RegisterSyntaxNodeAction( - context => VerifyBaseTypesSyntax(context.ReportDiagnostic, GetTypeSyntaxNodesFromBaseType(context.Node), context), + context => + { + if (ShouldSkipSyntaxNodeAnalysis(context)) + return; + + VerifyBaseTypesSyntax(context.ReportDiagnostic, GetTypeSyntaxNodesFromBaseType(context.Node), context); + }, BaseTypeSyntaxKinds); return; @@ -219,32 +245,79 @@ bool ContainsAttributeSymbol(ISymbol symbol) }; } + bool ShouldSkipOperationAnalysis(OperationAnalysisContext context) + => !ShouldAnalyzeInTree(context.Operation.Syntax.SyntaxTree, context.IsGeneratedCode, context.CancellationToken); + + bool ShouldSkipSyntaxNodeAnalysis(SyntaxNodeAnalysisContext context) + => !ShouldAnalyzeInTree(context.Node.SyntaxTree, context.IsGeneratedCode, context.CancellationToken); + + bool ShouldSkipTreeAnalysis(SyntaxTree tree, bool? isGeneratedCode, CancellationToken cancellationToken) + => !ShouldAnalyzeInTree(tree, isGeneratedCode, cancellationToken); + + bool ShouldAnalyzeInTree(SyntaxTree tree, bool? isGeneratedCode, CancellationToken cancellationToken) + { + if (isGeneratedCode == false) + { + _ = shouldAnalyzeMap.TryAdd(tree, true); + return true; + } + + return shouldAnalyzeMap.GetOrAdd( + tree, + tree => + { + var options = compilationContext.Options.AnalyzerConfigOptionsProvider.GetOptions(tree); + var excludesGeneratedCode = + options.TryGetValue(ExcludeGeneratedCodeOptionName, out var optionValue) && + bool.TryParse(optionValue, out var val) && + val; + + if (!excludesGeneratedCode) + { + return true; + } + + if (isGeneratedCode == true) + { + return false; + } + + var generatedCodeKind = GeneratedCodeUtilities.GetGeneratedCodeKindFromOptions(options); + return generatedCodeKind switch + { + GeneratedKind.MarkedGenerated => false, + GeneratedKind.NotGenerated => true, + _ => !GeneratedCodeUtilities.IsGeneratedCode(tree, IsRegularCommentOrDocumentationComment, cancellationToken), + }; + }); + } + void VerifyAttributes(Action reportDiagnostic, ImmutableArray attributes, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); foreach (var attribute in attributes) { + var applicationSyntaxReference = attribute.ApplicationSyntaxReference; + if (applicationSyntaxReference == null) + continue; + + if (ShouldSkipTreeAnalysis(applicationSyntaxReference.SyntaxTree, isGeneratedCode: null, cancellationToken)) + continue; + + var node = applicationSyntaxReference.GetSyntax(cancellationToken); + if (IsBannedSymbol(attribute.AttributeClass, out var entry)) { - var node = attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken); - if (node != null) - { - reportDiagnostic( - node.CreateDiagnostic( - SymbolIsBannedRule, - attribute.AttributeClass.ToDisplayString(), - string.IsNullOrWhiteSpace(entry.Message) ? "" : ": " + entry.Message)); - } + reportDiagnostic( + node.CreateDiagnostic( + SymbolIsBannedRule, + attribute.AttributeClass.ToDisplayString(), + string.IsNullOrWhiteSpace(entry.Message) ? "" : ": " + entry.Message)); } if (attribute.AttributeConstructor != null) { - var syntaxNode = attribute.ApplicationSyntaxReference?.GetSyntax(cancellationToken); - - if (syntaxNode != null) - { - VerifySymbol(reportDiagnostic, attribute.AttributeConstructor, syntaxNode); - } + VerifySymbol(reportDiagnostic, attribute.AttributeConstructor, node); } } } diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers.sarif b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers.sarif index 9fb79cc1aa5b..1b87c19ddbe1 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers.sarif +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers.sarif @@ -5,7 +5,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.BannedApiAnalyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -14,7 +14,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.CSharp.BannedApiAnalyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -77,7 +77,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.VisualBasic.BannedApiAnalyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/UnitTests/SymbolIsBannedAnalyzerTests.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/UnitTests/SymbolIsBannedAnalyzerTests.cs index 876acc5ed77c..875dba1c1492 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/UnitTests/SymbolIsBannedAnalyzerTests.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/UnitTests/SymbolIsBannedAnalyzerTests.cs @@ -63,6 +63,871 @@ private static async Task VerifyCSharpAnalyzerAsync(string source, string banned await test.RunAsync(); } + [Fact] + [WorkItem(82114, "https://github.com/dotnet/roslyn/issues/82114")] + public async Task DiagnosticReportedInGeneratedFileByDefaultAsync() + { + const string bannedText = "T:N.Banned"; + + var csharpTest = new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Generated.g.cs", """ + namespace N + { + class Banned + { + } + + class C + { + void M() + { + var c = {|#0:new Banned()|}; + } + } + } + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + }, + }; + + csharpTest.ExpectedDiagnostics.Add(GetCSharpResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await csharpTest.RunAsync(); + + var basicTest = new VerifyVB.Test + { + TestState = + { + Sources = + { + ("Generated.g.vb", """ + Namespace N + Class Banned + End Class + + Class C + Sub M() + Dim c = {|#0:New Banned()|} + End Sub + End Class + End Namespace + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + }, + }; + + basicTest.ExpectedDiagnostics.Add(GetBasicResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await basicTest.RunAsync(); + } + + [Fact] + [WorkItem(82114, "https://github.com/dotnet/roslyn/issues/82114")] + public async Task GeneratedFileSkippedWhenConfiguredAsync() + { + const string bannedText = "T:N.Banned"; + + var csharpTest = new VerifyCS.Test + { + TestState = + { + Sources = + { + ("/0/Shared.cs", """ + namespace N + { + class Banned + { + } + } + """), + ("/0/Generated.g.cs", """ + namespace N + { + class GeneratedUsage + { + void M() + { + var c = {|#0:new Banned()|}; + } + } + } + """), + ("/0/Test0.cs", """ + namespace N + { + class NongeneratedUsage + { + void M() + { + var c = {|#1:new Banned()|}; + } + } + } + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + csharpTest.ExpectedDiagnostics.Add(GetCSharpResultAt(1, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await csharpTest.RunAsync(); + + var basicTest = new VerifyVB.Test + { + TestState = + { + Sources = + { + ("/0/Shared.vb", """ + Namespace N + Class Banned + End Class + End Namespace + """), + ("/0/Generated.g.vb", """ + Namespace N + Class GeneratedUsage + Sub M() + Dim c = {|#0:New Banned()|} + End Sub + End Class + End Namespace + """), + ("/0/Test0.vb", """ + Namespace N + Class NongeneratedUsage + Sub M() + Dim c = {|#1:New Banned()|} + End Sub + End Class + End Namespace + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + basicTest.ExpectedDiagnostics.Add(GetBasicResultAt(1, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await basicTest.RunAsync(); + } + + [Fact] + [WorkItem(82114, "https://github.com/dotnet/roslyn/issues/82114")] + public async Task GeneratedFileSkippedWhenConfiguredInEditorConfigAsync() + { + const string bannedText = "T:N.Banned"; + + var csharpTest = new VerifyCS.Test + { + TestState = + { + Sources = + { + ("/0/Shared.cs", """ + namespace N + { + class Banned + { + } + } + """), + ("/0/Generated.g.cs", """ + namespace N + { + class GeneratedUsage + { + void M() + { + var c = {|#0:new Banned()|}; + } + } + } + """), + ("/0/Test0.cs", """ + namespace N + { + class NongeneratedUsage + { + void M() + { + var c = {|#1:new Banned()|}; + } + } + } + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.editorconfig", """ + root = true + + [*] + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + csharpTest.ExpectedDiagnostics.Add(GetCSharpResultAt(1, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await csharpTest.RunAsync(); + + var basicTest = new VerifyVB.Test + { + TestState = + { + Sources = + { + ("/0/Shared.vb", """ + Namespace N + Class Banned + End Class + End Namespace + """), + ("/0/Generated.g.vb", """ + Namespace N + Class GeneratedUsage + Sub M() + Dim c = {|#0:New Banned()|} + End Sub + End Class + End Namespace + """), + ("/0/Test0.vb", """ + Namespace N + Class NongeneratedUsage + Sub M() + Dim c = {|#1:New Banned()|} + End Sub + End Class + End Namespace + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.editorconfig", """ + root = true + + [*] + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + basicTest.ExpectedDiagnostics.Add(GetBasicResultAt(1, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await basicTest.RunAsync(); + } + + [Fact] + [WorkItem(82114, "https://github.com/dotnet/roslyn/issues/82114")] + public async Task GeneratedFileSkippedWhenConfiguredInEditorConfigForSpecificPathAsync() + { + const string bannedText = "T:N.Banned"; + + var csharpTest = new VerifyCS.Test + { + TestState = + { + Sources = + { + ("/0/Shared.cs", """ + namespace N + { + class Banned + { + } + } + """), + ("/0/Generated/Matched.g.cs", """ + namespace N + { + class MatchedGeneratedUsage + { + void M() + { + var c = {|#0:new Banned()|}; + } + } + } + """), + ("/0/Generated/Other.g.cs", """ + namespace N + { + class OtherGeneratedUsage + { + void M() + { + var c = {|#1:new Banned()|}; + } + } + } + """), + ("/0/Generated/Test0.cs", """ + namespace N + { + class NongeneratedUsage + { + void M() + { + var c = {|#2:new Banned()|}; + } + } + } + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.editorconfig", """ + root = true + + [*] + dotnet_banned_api_analyzer.exclude_generated_code = false + + [/0/Generated/Matched.g.cs] + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + csharpTest.ExpectedDiagnostics.Add(GetCSharpResultAt(1, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + csharpTest.ExpectedDiagnostics.Add(GetCSharpResultAt(2, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await csharpTest.RunAsync(); + + var basicTest = new VerifyVB.Test + { + TestState = + { + Sources = + { + ("/0/Shared.vb", """ + Namespace N + Class Banned + End Class + End Namespace + """), + ("/0/Generated/Matched.g.vb", """ + Namespace N + Class MatchedGeneratedUsage + Sub M() + Dim c = {|#0:New Banned()|} + End Sub + End Class + End Namespace + """), + ("/0/Generated/Other.g.vb", """ + Namespace N + Class OtherGeneratedUsage + Sub M() + Dim c = {|#1:New Banned()|} + End Sub + End Class + End Namespace + """), + ("/0/Generated/Test0.vb", """ + Namespace N + Class NongeneratedUsage + Sub M() + Dim c = {|#2:New Banned()|} + End Sub + End Class + End Namespace + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.editorconfig", """ + root = true + + [*] + dotnet_banned_api_analyzer.exclude_generated_code = false + + [/0/Generated/Matched.g.vb] + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + basicTest.ExpectedDiagnostics.Add(GetBasicResultAt(1, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + basicTest.ExpectedDiagnostics.Add(GetBasicResultAt(2, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await basicTest.RunAsync(); + } + + [Fact] + [WorkItem(82114, "https://github.com/dotnet/roslyn/issues/82114")] + public async Task GeneratedAttributeSkippedWhenConfiguredAsync() + { + const string bannedText = "T:BannedAttribute"; + + var csharpTest = new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Generated.g.cs", """ + using System; + + [{|#0:Banned|}] + class C + { + } + """), + ("Shared.cs", """ + using System; + + [AttributeUsage(AttributeTargets.All, Inherited = true)] + class BannedAttribute : Attribute + { + } + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + await csharpTest.RunAsync(); + + var basicTest = new VerifyVB.Test + { + TestState = + { + Sources = + { + ("Generated.g.vb", """ + Imports System + + <{|#0:Banned|}> + Class C + End Class + """), + ("Shared.vb", """ + Imports System + + + Class BannedAttribute + Inherits Attribute + End Class + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + await basicTest.RunAsync(); + } + + [Fact] + [WorkItem(82114, "https://github.com/dotnet/roslyn/issues/82114")] + public async Task GeneratedDeclarationDoesNotSkipNonGeneratedPartialSymbolAttributeAnalysisAsync() + { + const string bannedText = "T:BannedAttribute"; + + var csharpTest = new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Generated.g.cs", """ + partial class C + { + } + """), + ("Shared.cs", """ + using System; + + [AttributeUsage(AttributeTargets.All, Inherited = true)] + class BannedAttribute : Attribute + { + } + """), + ("Test0.cs", """ + [{|#0:Banned|}] + partial class C + { + } + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + csharpTest.ExpectedDiagnostics.Add(GetCSharpResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "BannedAttribute", "")); + csharpTest.ExpectedDiagnostics.Add(GetCSharpResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "BannedAttribute", "")); + await csharpTest.RunAsync(); + + var basicTest = new VerifyVB.Test + { + TestState = + { + Sources = + { + ("Generated.g.vb", """ + Partial Class C + End Class + """), + ("Shared.vb", """ + Imports System + + + Class BannedAttribute + Inherits Attribute + End Class + """), + ("Test0.vb", """ + <{|#0:Banned|}> + Partial Class C + End Class + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + basicTest.ExpectedDiagnostics.Add(GetBasicResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "BannedAttribute", "")); + basicTest.ExpectedDiagnostics.Add(GetBasicResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "BannedAttribute", "")); + await basicTest.RunAsync(); + } + + [Fact] + [WorkItem(82114, "https://github.com/dotnet/roslyn/issues/82114")] + public async Task GeneratedDeclarationDoesSkipsGeneratedPartialSymbolAttributeAnalysisAsync() + { + const string bannedText = "T:BannedAttribute"; + + var csharpTest = new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Generated.g.cs", """ + [Banned] + partial class C + { + } + """), + ("Shared.cs", """ + using System; + + [AttributeUsage(AttributeTargets.All, Inherited = true)] + class BannedAttribute : Attribute + { + } + """), + ("Test0.cs", """ + partial class C + { + } + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + await csharpTest.RunAsync(); + + var basicTest = new VerifyVB.Test + { + TestState = + { + Sources = + { + ("Generated.g.vb", """ + + Partial Class C + End Class + """), + ("Shared.vb", """ + Imports System + + + Class BannedAttribute + Inherits Attribute + End Class + """), + ("Test0.vb", """ + Partial Class C + End Class + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + await basicTest.RunAsync(); + } + + [Fact] + [WorkItem(82114, "https://github.com/dotnet/roslyn/issues/82114")] + public async Task GeneratedDeclarationDoesNotSkipNonGeneratedPartialSymbolUsageAnalysisAsync() + { + const string bannedText = "T:N.Banned"; + + var csharpTest = new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Generated.g.cs", """ + namespace N + { + partial class C + { + } + } + """), + ("Shared.cs", """ + namespace N + { + class Banned + { + } + } + """), + ("Test0.cs", """ + namespace N + { + partial class C + { + void M() + { + var c = {|#0:new Banned()|}; + } + } + } + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + csharpTest.ExpectedDiagnostics.Add(GetCSharpResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await csharpTest.RunAsync(); + + var basicTest = new VerifyVB.Test + { + TestState = + { + Sources = + { + ("Generated.g.vb", """ + Namespace N + Partial Class C + End Class + End Namespace + """), + ("Shared.vb", """ + Namespace N + Class Banned + End Class + End Namespace + """), + ("Test0.vb", """ + Namespace N + Partial Class C + Sub M() + Dim c = {|#0:New Banned()|} + End Sub + End Class + End Namespace + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + basicTest.ExpectedDiagnostics.Add(GetBasicResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await basicTest.RunAsync(); + } + + [Fact] + [WorkItem(82114, "https://github.com/dotnet/roslyn/issues/82114")] + public async Task GeneratedDeclarationDoesNotSkipNonGeneratedPartialSymbolBaseTypeAnalysisAsync() + { + const string bannedText = "T:N.Banned"; + + var csharpTest = new VerifyCS.Test + { + TestState = + { + Sources = + { + ("Generated.g.cs", """ + namespace N + { + partial class C + { + } + } + """), + ("Shared.cs", """ + namespace N + { + class Banned + { + } + } + """), + ("Test0.cs", """ + namespace N + { + partial class C : {|#0:Banned|} + { + } + } + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + csharpTest.ExpectedDiagnostics.Add(GetCSharpResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await csharpTest.RunAsync(); + + var basicTest = new VerifyVB.Test + { + TestState = + { + Sources = + { + ("Generated.g.vb", """ + Namespace N + Partial Class C + End Class + End Namespace + """), + ("Shared.vb", """ + Namespace N + Class Banned + End Class + End Namespace + """), + ("Test0.vb", """ + Namespace N + Partial Class C + Inherits {|#0:Banned|} + End Class + End Namespace + """), + }, + AdditionalFiles = { (BannedSymbolsFileName, bannedText) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", """ + is_global = true + + dotnet_banned_api_analyzer.exclude_generated_code = true + """), + }, + }, + }; + + basicTest.ExpectedDiagnostics.Add(GetBasicResultAt(0, SymbolIsBannedAnalyzer.SymbolIsBannedRule, "Banned", "")); + await basicTest.RunAsync(); + } + #region Diagnostic tests [Fact] diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/VisualBasic/BasicSymbolIsBannedAnalyzer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/VisualBasic/BasicSymbolIsBannedAnalyzer.vb index 79db7b962f4f..aa0a214d7cdd 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/VisualBasic/BasicSymbolIsBannedAnalyzer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.BannedApiAnalyzers/VisualBasic/BasicSymbolIsBannedAnalyzer.vb @@ -30,6 +30,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.BannedApiAnalyzers End Get End Property + Protected Overrides Function IsRegularCommentOrDocumentationComment(trivia As SyntaxTrivia) As Boolean + Return trivia.Kind() = SyntaxKind.CommentTrivia OrElse trivia.Kind() = SyntaxKind.DocumentationCommentTrivia + End Function + Protected Overrides Function GetReferenceSyntaxNodeFromXmlCref(syntaxNode As SyntaxNode) As SyntaxNode Return CType(syntaxNode, XmlCrefAttributeSyntax).Reference End Function diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsCSharpAsync_False/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsCSharpAsync_False/Resources.Designer.cs index ea32704b17f2..5ed47832434b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsCSharpAsync_False/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsCSharpAsync_False/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsCSharpAsync_True/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsCSharpAsync_True/Resources.Designer.cs index 33122d2e5e45..84b8ae55bafb 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsCSharpAsync_True/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsCSharpAsync_True/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsVisualBasicAsync_False/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsVisualBasicAsync_False/Resources.Designer.vb index 5c1dfab8ed12..532d73de6e83 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsVisualBasicAsync_False/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsVisualBasicAsync_False/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsVisualBasicAsync_True/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsVisualBasicAsync_True/Resources.Designer.vb index 45f47050b681..425a15d71bda 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsVisualBasicAsync_True/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_AsConstantsVisualBasicAsync_True/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameCSharpAsync/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameCSharpAsync/Resources.Designer.cs new file mode 100644 index 000000000000..115a9a90b70b --- /dev/null +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameCSharpAsync/Resources.Designer.cs @@ -0,0 +1,30 @@ +// + +#nullable enable +using System.Reflection; + +namespace TestProject +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + internal static partial class Resources + { + private static global::System.Resources.ResourceManager? s_resourceManager; + /// + /// Returns the cached ResourceManager instance used by this class. + /// + public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager("RootNS.Folder.File", typeof(Resources).Assembly)); + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + public static global::System.Globalization.CultureInfo? Culture { get; set; } + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("defaultValue")] + internal static string? GetResourceString(string resourceKey, string? defaultValue = null) => ResourceManager.GetString(resourceKey, Culture) ?? defaultValue; + /// value + public static string @Name => GetResourceString("Name")!; + + } +} \ No newline at end of file diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameCSharpAsync_NS/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameCSharpAsync_NS/Resources.Designer.cs new file mode 100644 index 000000000000..6cabeed58110 --- /dev/null +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameCSharpAsync_NS/Resources.Designer.cs @@ -0,0 +1,28 @@ +// + +#nullable enable +using System.Reflection; + + +/// +/// A strongly-typed resource class, for looking up localized strings, etc. +/// +internal static partial class NS +{ + private static global::System.Resources.ResourceManager? s_resourceManager; + /// + /// Returns the cached ResourceManager instance used by this class. + /// + public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager("RootNS.Folder.File", typeof(NS).Assembly)); + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + public static global::System.Globalization.CultureInfo? Culture { get; set; } + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("defaultValue")] + internal static string? GetResourceString(string resourceKey, string? defaultValue = null) => ResourceManager.GetString(resourceKey, Culture) ?? defaultValue; + /// value + public static string @Name => GetResourceString("Name")!; + +} diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameCSharpAsync_NS1.NS2/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameCSharpAsync_NS1.NS2/Resources.Designer.cs new file mode 100644 index 000000000000..366b2d02d8ab --- /dev/null +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameCSharpAsync_NS1.NS2/Resources.Designer.cs @@ -0,0 +1,30 @@ +// + +#nullable enable +using System.Reflection; + +namespace NS1 +{ + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + internal static partial class NS2 + { + private static global::System.Resources.ResourceManager? s_resourceManager; + /// + /// Returns the cached ResourceManager instance used by this class. + /// + public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager("RootNS.Folder.File", typeof(NS2).Assembly)); + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + public static global::System.Globalization.CultureInfo? Culture { get; set; } + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + [return: global::System.Diagnostics.CodeAnalysis.NotNullIfNotNull("defaultValue")] + internal static string? GetResourceString(string resourceKey, string? defaultValue = null) => ResourceManager.GetString(resourceKey, Culture) ?? defaultValue; + /// value + public static string @Name => GetResourceString("Name")!; + + } +} \ No newline at end of file diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameVisualBasicAsync/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameVisualBasicAsync/Resources.Designer.vb new file mode 100644 index 000000000000..bc9ac09354f0 --- /dev/null +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameVisualBasicAsync/Resources.Designer.vb @@ -0,0 +1,42 @@ +' + +Imports System.Reflection + +Namespace Global.TestProject + ''' + ''' A strongly-typed resource class, for looking up localized strings, etc. + ''' + Friend Partial Class Resources + Private Sub New + End Sub + + Private Shared s_resourceManager As Global.System.Resources.ResourceManager + ''' + ''' Returns the cached ResourceManager instance used by this class. + ''' + Public Shared ReadOnly Property ResourceManager As Global.System.Resources.ResourceManager + Get + If s_resourceManager Is Nothing Then + s_resourceManager = New Global.System.Resources.ResourceManager("RootNS.Folder.File", GetType(Resources).Assembly) + End If + Return s_resourceManager + End Get + End Property + ''' + ''' Overrides the current thread's CurrentUICulture property for all + ''' resource lookups using this strongly typed resource class. + ''' + Public Shared Property Culture As Global.System.Globalization.CultureInfo + + Friend Shared Function GetResourceString(ByVal resourceKey As String, Optional ByVal defaultValue As String = Nothing) As String + Return ResourceManager.GetString(resourceKey, Culture) + End Function + ''' value + Public Shared ReadOnly Property [Name] As String + Get + Return GetResourceString("Name") + End Get + End Property + + End Class +End Namespace \ No newline at end of file diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameVisualBasicAsync_NS/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameVisualBasicAsync_NS/Resources.Designer.vb new file mode 100644 index 000000000000..dbaa5ee7660c --- /dev/null +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameVisualBasicAsync_NS/Resources.Designer.vb @@ -0,0 +1,41 @@ +' + +Imports System.Reflection + + +''' +''' A strongly-typed resource class, for looking up localized strings, etc. +''' +Friend Partial Class NS + Private Sub New + End Sub + + Private Shared s_resourceManager As Global.System.Resources.ResourceManager + ''' + ''' Returns the cached ResourceManager instance used by this class. + ''' + Public Shared ReadOnly Property ResourceManager As Global.System.Resources.ResourceManager + Get + If s_resourceManager Is Nothing Then + s_resourceManager = New Global.System.Resources.ResourceManager("RootNS.Folder.File", GetType(NS).Assembly) + End If + Return s_resourceManager + End Get + End Property + ''' + ''' Overrides the current thread's CurrentUICulture property for all + ''' resource lookups using this strongly typed resource class. + ''' + Public Shared Property Culture As Global.System.Globalization.CultureInfo + + Friend Shared Function GetResourceString(ByVal resourceKey As String, Optional ByVal defaultValue As String = Nothing) As String + Return ResourceManager.GetString(resourceKey, Culture) + End Function + ''' value + Public Shared ReadOnly Property [Name] As String + Get + Return GetResourceString("Name") + End Get + End Property + +End Class diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameVisualBasicAsync_NS1.NS2/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameVisualBasicAsync_NS1.NS2/Resources.Designer.vb new file mode 100644 index 000000000000..de7f741c304d --- /dev/null +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameAndResourceNameVisualBasicAsync_NS1.NS2/Resources.Designer.vb @@ -0,0 +1,42 @@ +' + +Imports System.Reflection + +Namespace Global.NS1 + ''' + ''' A strongly-typed resource class, for looking up localized strings, etc. + ''' + Friend Partial Class NS2 + Private Sub New + End Sub + + Private Shared s_resourceManager As Global.System.Resources.ResourceManager + ''' + ''' Returns the cached ResourceManager instance used by this class. + ''' + Public Shared ReadOnly Property ResourceManager As Global.System.Resources.ResourceManager + Get + If s_resourceManager Is Nothing Then + s_resourceManager = New Global.System.Resources.ResourceManager("RootNS.Folder.File", GetType(NS2).Assembly) + End If + Return s_resourceManager + End Get + End Property + ''' + ''' Overrides the current thread's CurrentUICulture property for all + ''' resource lookups using this strongly typed resource class. + ''' + Public Shared Property Culture As Global.System.Globalization.CultureInfo + + Friend Shared Function GetResourceString(ByVal resourceKey As String, Optional ByVal defaultValue As String = Nothing) As String + Return ResourceManager.GetString(resourceKey, Culture) + End Function + ''' value + Public Shared ReadOnly Property [Name] As String + Get + Return GetResourceString("Name") + End Get + End Property + + End Class +End Namespace \ No newline at end of file diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync/Resources.Designer.cs index ea32704b17f2..5ed47832434b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync_NS/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync_NS/Resources.Designer.cs index d7731db46160..65a0e4d8074a 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync_NS/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync_NS/Resources.Designer.cs @@ -3,10 +3,6 @@ #nullable enable using System.Reflection; -namespace TestProject -{ - internal static class Resources { } -} /// /// A strongly-typed resource class, for looking up localized strings, etc. @@ -17,7 +13,7 @@ internal static partial class NS /// /// Returns the cached ResourceManager instance used by this class. /// - public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager(typeof(TestProject.Resources))); + public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager("TestProject.Resources", typeof(NS).Assembly)); /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync_NS1.NS2/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync_NS1.NS2/Resources.Designer.cs index b31421a7ddf3..966295c52fd4 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync_NS1.NS2/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameCSharpAsync_NS1.NS2/Resources.Designer.cs @@ -3,10 +3,6 @@ #nullable enable using System.Reflection; -namespace TestProject -{ - internal static class Resources { } -} namespace NS1 { /// @@ -18,7 +14,7 @@ internal static partial class NS2 /// /// Returns the cached ResourceManager instance used by this class. /// - public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager(typeof(TestProject.Resources))); + public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager("TestProject.Resources", typeof(NS2).Assembly)); /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync/Resources.Designer.vb index 5c1dfab8ed12..532d73de6e83 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync_NS/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync_NS/Resources.Designer.vb index b408efc3c08e..44dbe51e239e 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync_NS/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync_NS/Resources.Designer.vb @@ -2,10 +2,6 @@ Imports System.Reflection -Namespace TestProject - Friend Class Resources - End Class -End Namespace ''' ''' A strongly-typed resource class, for looking up localized strings, etc. @@ -21,7 +17,7 @@ Friend Partial Class NS Public Shared ReadOnly Property ResourceManager As Global.System.Resources.ResourceManager Get If s_resourceManager Is Nothing Then - s_resourceManager = New Global.System.Resources.ResourceManager(GetType(TestProject.Resources)) + s_resourceManager = New Global.System.Resources.ResourceManager("TestProject.Resources", GetType(NS).Assembly) End If Return s_resourceManager End Get diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync_NS1.NS2/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync_NS1.NS2/Resources.Designer.vb index 9ecb73fc1c82..d246ec6e470b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync_NS1.NS2/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_ClassNameVisualBasicAsync_NS1.NS2/Resources.Designer.vb @@ -2,10 +2,6 @@ Imports System.Reflection -Namespace TestProject - Friend Class Resources - End Class -End Namespace Namespace Global.NS1 ''' ''' A strongly-typed resource class, for looking up localized strings, etc. @@ -21,7 +17,7 @@ Namespace Global.NS1 Public Shared ReadOnly Property ResourceManager As Global.System.Resources.ResourceManager Get If s_resourceManager Is Nothing Then - s_resourceManager = New Global.System.Resources.ResourceManager(GetType(TestProject.Resources)) + s_resourceManager = New Global.System.Resources.ResourceManager("TestProject.Resources", GetType(NS2).Assembly) End If Return s_resourceManager End Get diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp5/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp5/Resources.Designer.cs index e21b3f2346f8..eaa7ae86845a 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp5/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp5/Resources.Designer.cs @@ -1,4 +1,4 @@ -// +// using System.Reflection; diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp6/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp6/Resources.Designer.cs index e21b3f2346f8..2e3c11e2eab8 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp6/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp6/Resources.Designer.cs @@ -3,7 +3,6 @@ using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp7/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp7/Resources.Designer.cs index e21b3f2346f8..2e3c11e2eab8 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp7/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp7/Resources.Designer.cs @@ -3,7 +3,6 @@ using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp8/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp8/Resources.Designer.cs index ea32704b17f2..5ed47832434b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp8/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp8/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp9/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp9/Resources.Designer.cs index ea32704b17f2..5ed47832434b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp9/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultCSharpAsync_CSharp9/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultVisualBasicAsync/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultVisualBasicAsync/Resources.Designer.vb index 5c1dfab8ed12..532d73de6e83 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultVisualBasicAsync/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_DefaultVisualBasicAsync/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_0_False/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_0_False/Resources.Designer.cs index 44dbc0fd8e25..f4dd089a65cc 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_0_False/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_0_False/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_0_True/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_0_True/Resources.Designer.cs index 37b7f3698ea9..99b02057bbf8 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_0_True/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_0_True/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_replacement_False/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_replacement_False/Resources.Designer.cs index cd59ebaff771..04ca1df823c6 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_replacement_False/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_replacement_False/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_replacement_True/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_replacement_True/Resources.Designer.cs index 7db044991788..87d0f370b485 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_replacement_True/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_replacement_True/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_x_False/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_x_False/Resources.Designer.cs index 4cc44c5860b1..b2c7a11fe979 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_x_False/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_x_False/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_x_True/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_x_True/Resources.Designer.cs index 54448dbbae77..d84a7adfba4e 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_x_True/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsCSharpAsync_x_True/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsVisualBasicAsync_False/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsVisualBasicAsync_False/Resources.Designer.vb index 5c1dfab8ed12..532d73de6e83 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsVisualBasicAsync_False/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_EmitFormatMethodsVisualBasicAsync_False/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesCSharpAsync_False/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesCSharpAsync_False/Resources.Designer.cs index ea32704b17f2..5ed47832434b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesCSharpAsync_False/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesCSharpAsync_False/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesCSharpAsync_True/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesCSharpAsync_True/Resources.Designer.cs index 22c8126d62c0..d9be0d4a331a 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesCSharpAsync_True/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesCSharpAsync_True/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesVisualBasicAsync_False/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesVisualBasicAsync_False/Resources.Designer.vb index 5c1dfab8ed12..532d73de6e83 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesVisualBasicAsync_False/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesVisualBasicAsync_False/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesVisualBasicAsync_True/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesVisualBasicAsync_True/Resources.Designer.vb index 8d256fd62fc9..94265334763f 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesVisualBasicAsync_True/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_IncludeDefaultValuesVisualBasicAsync_True/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync/Resources.Designer.cs index ea32704b17f2..5ed47832434b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync_CS1591/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync_CS1591/Resources.Designer.cs index 96d67c95dc90..09dbe5a24b9b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync_CS1591/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync_CS1591/Resources.Designer.cs @@ -5,7 +5,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync_CS1591_IDE0010/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync_CS1591_IDE0010/Resources.Designer.cs index 955ccfe4674f..6ea12094cb8c 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync_CS1591_IDE0010/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsCSharpAsync_CS1591_IDE0010/Resources.Designer.cs @@ -5,7 +5,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync/Resources.Designer.vb index 5c1dfab8ed12..532d73de6e83 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync_CS1591/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync_CS1591/Resources.Designer.vb index e4a7fb4ead33..82f55290cdb9 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync_CS1591/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync_CS1591/Resources.Designer.vb @@ -4,7 +4,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync_CS1591_IDE0010/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync_CS1591_IDE0010/Resources.Designer.vb index 7c1586d010e0..8ebfd7f036ef 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync_CS1591_IDE0010/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_NoWarnsVisualBasicAsync_CS1591_IDE0010/Resources.Designer.vb @@ -4,7 +4,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringCSharpAsync_False/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringCSharpAsync_False/Resources.Designer.cs index ea32704b17f2..5ed47832434b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringCSharpAsync_False/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringCSharpAsync_False/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringCSharpAsync_True/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringCSharpAsync_True/Resources.Designer.cs index acae1b789d45..dfd620bb6928 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringCSharpAsync_True/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringCSharpAsync_True/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringVisualBasicAsync_False/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringVisualBasicAsync_False/Resources.Designer.vb index 5c1dfab8ed12..532d73de6e83 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringVisualBasicAsync_False/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringVisualBasicAsync_False/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringVisualBasicAsync_True/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringVisualBasicAsync_True/Resources.Designer.vb index 9d3bde4c0f8c..a00e74214e2e 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringVisualBasicAsync_True/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_OmitGetResourceStringVisualBasicAsync_True/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicCSharpAsync_False/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicCSharpAsync_False/Resources.Designer.cs index ea32704b17f2..5ed47832434b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicCSharpAsync_False/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicCSharpAsync_False/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicCSharpAsync_True/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicCSharpAsync_True/Resources.Designer.cs index 20d17eb2ff53..b5e7f0009998 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicCSharpAsync_True/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicCSharpAsync_True/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicVisualBasicAsync_False/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicVisualBasicAsync_False/Resources.Designer.vb index 5c1dfab8ed12..532d73de6e83 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicVisualBasicAsync_False/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicVisualBasicAsync_False/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicVisualBasicAsync_True/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicVisualBasicAsync_True/Resources.Designer.vb index f91e248858f5..d72cbe7735ed 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicVisualBasicAsync_True/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_PublicVisualBasicAsync_True/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync/Resources.Designer.cs index ea32704b17f2..5ed47832434b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync_NS/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync_NS/Resources.Designer.cs index 129f3d53b1b6..3fbf471591d1 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync_NS/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync_NS/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync_NS1.NS2/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync_NS1.NS2/Resources.Designer.cs index 94a1008f45b1..d6ce8cb2956d 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync_NS1.NS2/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirCSharpAsync_NS1.NS2/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject.NS1 { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync/Resources.Designer.vb index 5c1dfab8ed12..532d73de6e83 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync_NS/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync_NS/Resources.Designer.vb index 9ca2dc99f6e7..2a89fa8e2d87 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync_NS/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync_NS/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync_NS1.NS2/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync_NS1.NS2/Resources.Designer.vb index 8450b011b8c5..acc158b38f66 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync_NS1.NS2/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RelativeDirVisualBasicAsync_NS1.NS2/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject.NS1 ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync/Resources.Designer.cs index 16317ff5bb93..0353fef80454 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync/Resources.Designer.cs @@ -4,7 +4,6 @@ using System.Reflection; - /// /// A strongly-typed resource class, for looking up localized strings, etc. /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync_NS/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync_NS/Resources.Designer.cs index 5fcbcc52ee19..b6e948b1b15b 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync_NS/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync_NS/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace NS { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync_NS1.NS2/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync_NS1.NS2/Resources.Designer.cs index 5009648b2be9..a2492bcb9d4e 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync_NS1.NS2/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceCSharpAsync_NS1.NS2/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace NS1.NS2 { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync/Resources.Designer.vb index 8f9a3b0aa9a2..b8bdb86a7a85 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync/Resources.Designer.vb @@ -3,7 +3,6 @@ Imports System.Reflection - ''' ''' A strongly-typed resource class, for looking up localized strings, etc. ''' diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync_NS/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync_NS/Resources.Designer.vb index 3afec8fa5430..4e68c69c4ccf 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync_NS/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync_NS/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.NS ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync_NS1.NS2/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync_NS1.NS2/Resources.Designer.vb index 3d21dd810b48..642da15983e5 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync_NS1.NS2/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/SingleString_RootNamespaceVisualBasicAsync_NS1.NS2/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.NS1.NS2 ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultCSharpAsync/Resources.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultCSharpAsync/Resources.Designer.cs index 18ed09d6fcf4..7e9c27c9ad62 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultCSharpAsync/Resources.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultCSharpAsync/Resources.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject.First { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultCSharpAsync/Resources0.Designer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultCSharpAsync/Resources0.Designer.cs index 2304fb82b75c..f057806741e0 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultCSharpAsync/Resources0.Designer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultCSharpAsync/Resources0.Designer.cs @@ -3,7 +3,6 @@ #nullable enable using System.Reflection; - namespace TestProject.Second { /// diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultVisualBasicAsync/Resources.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultVisualBasicAsync/Resources.Designer.vb index 03398a304dae..54f00e12206e 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultVisualBasicAsync/Resources.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultVisualBasicAsync/Resources.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject.First ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultVisualBasicAsync/Resources0.Designer.vb b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultVisualBasicAsync/Resources0.Designer.vb index 9e8add145e51..8334ca19fa45 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultVisualBasicAsync/Resources0.Designer.vb +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/Resources/TwoResourcesSameName_DefaultVisualBasicAsync/Resources0.Designer.vb @@ -2,7 +2,6 @@ Imports System.Reflection - Namespace Global.TestProject.Second ''' ''' A strongly-typed resource class, for looking up localized strings, etc. diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/ResxGeneratorTests.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/ResxGeneratorTests.cs index 55bea2fa656a..1be05b945928 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/ResxGeneratorTests.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator.UnitTests/ResxGeneratorTests.cs @@ -420,6 +420,40 @@ public async Task SingleString_ClassNameCSharpAsync(string className) }.AddGeneratedSources().RunAsync(); } + [Theory] + [InlineData("")] + [InlineData("NS")] + [InlineData("NS1.NS2")] + public async Task SingleString_ClassNameAndResourceNameCSharpAsync(string className) + { + var code = ResxHeader + + """ + + value + comment + + """ + + ResxFooter; + + await new VerifyCS.Test(identifier: className) + { + TestState = + { + AdditionalFiles = { ("/0/Resources.resx", code) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", $""" + is_global = true + + [/0/Resources.resx] + build_metadata.AdditionalFiles.ClassName = {className} + build_metadata.AdditionalFiles.ManifestResourceName = RootNS.Folder.File + """), + }, + }, + }.AddGeneratedSources().RunAsync(); + } + [Theory] [InlineData("")] [InlineData("NS")] @@ -453,6 +487,40 @@ public async Task SingleString_ClassNameVisualBasicAsync(string className) }.AddGeneratedSources().RunAsync(); } + [Theory] + [InlineData("")] + [InlineData("NS")] + [InlineData("NS1.NS2")] + public async Task SingleString_ClassNameAndResourceNameVisualBasicAsync(string className) + { + var code = ResxHeader + + """ + + value + comment + + """ + + ResxFooter; + + await new VerifyVB.Test(identifier: className) + { + TestState = + { + AdditionalFiles = { ("/0/Resources.resx", code) }, + AnalyzerConfigFiles = + { + ("/.globalconfig", $""" + is_global = true + + [/0/Resources.resx] + build_metadata.AdditionalFiles.ClassName = {className} + build_metadata.AdditionalFiles.ManifestResourceName = RootNS.Folder.File + """), + }, + }, + }.AddGeneratedSources().RunAsync(); + } + [Theory] [CombinatorialData] public async Task SingleString_OmitGetResourceStringCSharpAsync(bool omitGetResourceString) diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator/AbstractResxGenerator.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator/AbstractResxGenerator.cs index 1db5557e54b8..28d49a0bd6f7 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator/AbstractResxGenerator.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.ResxSourceGenerator/Microsoft.CodeAnalysis.ResxSourceGenerator/AbstractResxGenerator.cs @@ -80,13 +80,56 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } var resourceHintName = Path.GetFileNameWithoutExtension(resourceFile.Path); - var resourceName = resourceHintName; - if (options.TryGetValue("build_metadata.AdditionalFiles.RelativeDir", out var relativeDir)) + string defaultResourceAndClassName; + if (options.TryGetValue("build_metadata.AdditionalFiles.Link", out var link) && link.Length > 0) { - resourceName = relativeDir.Replace(Path.DirectorySeparatorChar, '.').Replace(Path.AltDirectorySeparatorChar, '.') + resourceName; + resourceHintName = Path.GetFileNameWithoutExtension(link); + string linkRelativeDir = Path.GetDirectoryName(link); + if (linkRelativeDir.Length > 0 && !linkRelativeDir.EndsWith("\\", StringComparison.Ordinal)) + { + linkRelativeDir += '\\'; + } + + defaultResourceAndClassName = RelativeDirToName(linkRelativeDir, resourceHintName); + } + else if (options.TryGetValue("build_metadata.AdditionalFiles.RelativeDir", out var relativeDir) && !relativeDir.StartsWith("..", StringComparison.Ordinal) && !Path.IsPathRooted(relativeDir)) + { + defaultResourceAndClassName = RelativeDirToName(relativeDir, resourceHintName); + } + else + { + // This resx file comes from outside the project directory, and there's no Link metadata. + // Treat it like it's in the project directory. + defaultResourceAndClassName = resourceHintName; } - options.TryGetValue("build_metadata.AdditionalFiles.ClassName", out var resourceClassName); + static string RelativeDirToName(string relativeDir, string hintName) + { + string candidate = relativeDir.Replace(Path.DirectorySeparatorChar, '.').Replace(Path.AltDirectorySeparatorChar, '.') + hintName; + + // Make sure there are never two periods in a row. + while (candidate.Contains("..")) + { + candidate = candidate.Replace("..", "."); + } + + return candidate; + } + + if (!string.IsNullOrEmpty(rootNamespace)) + { + defaultResourceAndClassName = $"{rootNamespace}.{defaultResourceAndClassName}"; + } + + if (!options.TryGetValue("build_metadata.AdditionalFiles.ManifestResourceName", out var resourceName) || resourceName.Length == 0) + { + resourceName = defaultResourceAndClassName; + } + + if (!options.TryGetValue("build_metadata.AdditionalFiles.ClassName", out var resourceClassName) || resourceClassName.Length == 0) + { + resourceClassName = defaultResourceAndClassName; + } if (!options.TryGetValue("build_metadata.AdditionalFiles.OmitGetResourceString", out var omitGetResourceStringText) || !bool.TryParse(omitGetResourceStringText, out var omitGetResourceString)) @@ -129,7 +172,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) new ResourceInformation( CompilationInformation: compilationInfo, ResourceFile: resourceFile, - ResourceName: string.IsNullOrEmpty(rootNamespace) ? resourceName : string.Join(".", rootNamespace, resourceName), + ResourceName: resourceName, ResourceHintName: resourceHintName, ResourceClassName: resourceClassName, OmitGetResourceString: omitGetResourceString, @@ -237,7 +280,7 @@ private static bool IsExplicitWithCulture(AnalyzerConfigOptions options) } /// - /// + /// /// /// /// Language of source file to generate. Supported languages: CSharp, VisualBasic. @@ -250,7 +293,7 @@ private sealed record CompilationInformation( bool HasNotNullIfNotNull); /// - /// + /// /// /// Information about the compilation. /// Resources (resx) file. @@ -589,61 +632,6 @@ public bool Execute(CancellationToken cancellationToken) } } - string resourceTypeName; - string? resourceTypeDefinition; - if (string.IsNullOrEmpty(ResourceInformation.ResourceClassName) || ResourceInformation.ResourceName == ResourceInformation.ResourceClassName) - { - // resource name is same as accessor, no need for a second type. - resourceTypeName = className; - resourceTypeDefinition = null; - } - else - { - // resource name differs from the access class, need a type for specifying the resources - // this empty type must remain as it is required by the .NETNative toolchain for locating resources - // once assemblies have been merged into the application - resourceTypeName = ResourceInformation.ResourceName; - - SplitName(resourceTypeName, out var resourceNamespaceName, out var resourceClassName); - var resourceClassIndent = resourceNamespaceName == null ? "" : " "; - - switch (language) - { - case Lang.CSharp: - resourceTypeDefinition = $"{resourceClassIndent}internal static class {resourceClassName} {{ }}"; - if (resourceNamespaceName != null) - { - resourceTypeDefinition = $$""" - namespace {{resourceNamespaceName}} - { - {{resourceTypeDefinition}} - } - """; - } - - break; - - case Lang.VisualBasic: - resourceTypeDefinition = $""" - {resourceClassIndent}Friend Class {resourceClassName} - {resourceClassIndent}End Class - """; - if (resourceNamespaceName != null) - { - resourceTypeDefinition = $""" - Namespace {resourceNamespaceName} - {resourceTypeDefinition} - End Namespace - """; - } - - break; - - default: - throw new InvalidOperationException(); - } - } - // List of NoWarn string? noWarnDisabled = null; string? noWarnRestored = null; @@ -673,16 +661,19 @@ End Namespace // completely remove the ResourceManager class if the disk space saving optimization to strip resources // (/DisableExceptionMessages) is turned on in the compiler. string result; + string resourceManagerArguments; switch (language) { case Lang.CSharp: + resourceManagerArguments = ResourceInformation.ResourceName == ResourceInformation.ResourceClassName + ? $"typeof({className})" + : $"\"{ResourceInformation.ResourceName}\", typeof({className}).Assembly"; result = $$""" // {{noWarnDisabled}} {{(CompilationInformation.SupportsNullable ? "#nullable enable" : "")}} using System.Reflection; - {{resourceTypeDefinition}} {{namespaceStart}} {{classIndent}}/// {{classIndent}}/// A strongly-typed resource class, for looking up localized strings, etc. @@ -693,7 +684,7 @@ End Namespace {{memberIndent}}/// {{memberIndent}}/// Returns the cached ResourceManager instance used by this class. {{memberIndent}}/// - {{memberIndent}}public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager(typeof({{resourceTypeName}}))); + {{memberIndent}}public static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager({{resourceManagerArguments}})); {{getStringMethod}} {{strings}} {{classIndent}}} @@ -702,12 +693,14 @@ End Namespace break; case Lang.VisualBasic: + resourceManagerArguments = ResourceInformation.ResourceName == ResourceInformation.ResourceClassName + ? $"GetType({className})" + : $"\"{ResourceInformation.ResourceName}\", GetType({className}).Assembly"; result = $""" ' {noWarnDisabled} Imports System.Reflection - {resourceTypeDefinition} {namespaceStart} {classIndent}''' {classIndent}''' A strongly-typed resource class, for looking up localized strings, etc. @@ -723,7 +716,7 @@ Imports System.Reflection {memberIndent}Public Shared ReadOnly Property ResourceManager As Global.System.Resources.ResourceManager {memberIndent} Get {memberIndent} If s_resourceManager Is Nothing Then - {memberIndent} s_resourceManager = New Global.System.Resources.ResourceManager(GetType({resourceTypeName})) + {memberIndent} s_resourceManager = New Global.System.Resources.ResourceManager({resourceManagerArguments}) {memberIndent} End If {memberIndent} Return s_resourceManager {memberIndent} End Get diff --git a/src/RoslynAnalyzers/PerformanceSensitiveAnalyzers/Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers.sarif b/src/RoslynAnalyzers/PerformanceSensitiveAnalyzers/Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers.sarif index 8e4325f2a860..6cafd323014f 100644 --- a/src/RoslynAnalyzers/PerformanceSensitiveAnalyzers/Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers.sarif +++ b/src/RoslynAnalyzers/PerformanceSensitiveAnalyzers/Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers.sarif @@ -5,7 +5,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.CSharp.PerformanceSensitiveAnalyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -184,7 +184,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.CSharp.PerformanceSensitiveAnalyzers.CodeFixes", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -193,7 +193,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { diff --git a/src/RoslynAnalyzers/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs b/src/RoslynAnalyzers/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs index a491bae5f232..7e5db2c75819 100644 --- a/src/RoslynAnalyzers/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs +++ b/src/RoslynAnalyzers/PublicApiAnalyzers/Core/Analyzers/DeclarePublicApiAnalyzer.Impl.cs @@ -594,15 +594,17 @@ private static bool UsesOblivious(ISymbol symbol) private ApiName GetApiName(ISymbol symbol) { var experimentName = getExperimentName(symbol); + var requiresUnsafe = getRequiresUnsafe(symbol); return new ApiName( - getApiString(_compilation, symbol, experimentName, s_publicApiFormat), - getApiString(_compilation, symbol, experimentName, s_publicApiFormatWithNullability)); + getApiString(_compilation, symbol, experimentName, requiresUnsafe, s_publicApiFormat), + getApiString(_compilation, symbol, experimentName, requiresUnsafe, s_publicApiFormatWithNullability)); static string? getExperimentName(ISymbol symbol) { for (var current = symbol; current is not null; current = current.ContainingSymbol) { +start: foreach (var attribute in current.GetAttributes()) { if (attribute.AttributeClass is { Name: "ExperimentalAttribute", ContainingSymbol: INamespaceSymbol { Name: nameof(System.Diagnostics.CodeAnalysis), ContainingNamespace: { Name: nameof(System.Diagnostics), ContainingNamespace: { Name: nameof(System), ContainingNamespace.IsGlobalNamespace: true } } } }) @@ -611,15 +613,40 @@ private ApiName GetApiName(ISymbol symbol) return "???"; return diagnosticId; - } } + + if (current is IMethodSymbol { AssociatedSymbol: { } associatedSymbol }) + { + current = associatedSymbol; + goto start; + } } return null; } - static string getApiString(Compilation compilation, ISymbol symbol, string? experimentName, SymbolDisplayFormat format) + static bool getRequiresUnsafe(ISymbol symbol) + { + foreach (var attribute in symbol.GetAttributes()) + { + // https://github.com/dotnet/roslyn/issues/82546: Confirm the attribute shape in BCL API review. + // https://github.com/dotnet/roslyn/issues/82791: Use the public Roslyn API when available. + if (attribute.AttributeClass is { Name: "RequiresUnsafeAttribute", ContainingSymbol: INamespaceSymbol { Name: "CompilerServices", ContainingNamespace: { Name: "Runtime", ContainingNamespace: { Name: nameof(System), ContainingNamespace.IsGlobalNamespace: true } } } }) + { + return true; + } + } + + if (symbol is IMethodSymbol { AssociatedSymbol: { } associatedSymbol }) + { + return getRequiresUnsafe(associatedSymbol); + } + + return false; + } + + static string getApiString(Compilation compilation, ISymbol symbol, string? experimentName, bool requiresUnsafe, SymbolDisplayFormat format) { string publicApiName = symbol.ToDisplayString(format); @@ -661,6 +688,11 @@ static string getApiString(Compilation compilation, ISymbol symbol, string? expe publicApiName = "[" + experimentName + "]" + publicApiName; } + if (requiresUnsafe) + { + publicApiName = "[RequiresUnsafe]" + publicApiName; + } + return publicApiName; } } diff --git a/src/RoslynAnalyzers/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.sarif b/src/RoslynAnalyzers/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.sarif index 6d1a135303ef..c936894da031 100644 --- a/src/RoslynAnalyzers/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.sarif +++ b/src/RoslynAnalyzers/PublicApiAnalyzers/Microsoft.CodeAnalysis.PublicApiAnalyzers.sarif @@ -5,7 +5,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.PublicApiAnalyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -460,7 +460,7 @@ { "tool": { "name": "Microsoft.CodeAnalysis.PublicApiAnalyzers.CodeFixes", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { diff --git a/src/RoslynAnalyzers/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs b/src/RoslynAnalyzers/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs index 3e1174ef8e70..8cd3b40dcd4c 100644 --- a/src/RoslynAnalyzers/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs +++ b/src/RoslynAnalyzers/PublicApiAnalyzers/UnitTests/DeclarePublicAPIAnalyzerTestsBase.cs @@ -2783,6 +2783,112 @@ public Task TestExperimentalApiAsync() [ID1]C.C() -> void """); + [Fact] + [WorkItem(82131, "https://github.com/dotnet/roslyn/pull/82131")] + public Task TestExperimentalApi_PropertyAsync() + => VerifyNet80CSharpAdditionalFileFixAsync($$""" + using System.Diagnostics.CodeAnalysis; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [Experimental("ID1")] + {{EnabledModifierCSharp}} int Property { {|{{AddNewApiId}}:get|}; {|{{AddNewApiId}}:set|}; } + } + """, @"", @"", """ + C + C.C() -> void + [ID1]C.Property.get -> int + [ID1]C.Property.set -> void + """); + + [Fact] + [WorkItem(82131, "https://github.com/dotnet/roslyn/pull/82131")] + public Task TestExperimentalApi_PropertyGetterOnlyAsync() + => VerifyNet80CSharpAdditionalFileFixAsync($$""" + using System.Diagnostics.CodeAnalysis; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [Experimental("ID1")] + {{EnabledModifierCSharp}} int Property { {|{{AddNewApiId}}:get|}; } + } + """, @"", @"", """ + C + C.C() -> void + [ID1]C.Property.get -> int + """); + + [Fact] + [WorkItem(82131, "https://github.com/dotnet/roslyn/pull/82131")] + public Task TestExperimentalApi_PropertySetterOnlyAsync() + => VerifyNet80CSharpAdditionalFileFixAsync($$""" + using System.Diagnostics.CodeAnalysis; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [Experimental("ID1")] + {{EnabledModifierCSharp}} int Property { {|{{AddNewApiId}}:set|} { } } + } + """, @"", @"", """ + C + C.C() -> void + [ID1]C.Property.set -> void + """); + + [Fact] + [WorkItem(82131, "https://github.com/dotnet/roslyn/pull/82131")] + public Task TestExperimentalApi_IndexerAsync() + => VerifyNet80CSharpAdditionalFileFixAsync($$""" + using System.Diagnostics.CodeAnalysis; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [Experimental("ID1")] + {{EnabledModifierCSharp}} int this[int index] { {|{{AddNewApiId}}:get|} => 0; {|{{AddNewApiId}}:set|} { } } + } + """, @"", @"", """ + C + C.C() -> void + [ID1]C.this[int index].get -> int + [ID1]C.this[int index].set -> void + """); + + [Fact] + [WorkItem(82131, "https://github.com/dotnet/roslyn/pull/82131")] + public Task TestExperimentalApi_EventAsync() + => VerifyNet80CSharpAdditionalFileFixAsync($$""" + using System; + using System.Diagnostics.CodeAnalysis; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [Experimental("ID1")] + {{EnabledModifierCSharp}} event EventHandler {|{{AddNewApiId}}:MyEvent|}; + } + """, @"", @"", """ + C + C.C() -> void + [ID1]C.MyEvent -> System.EventHandler + """); + + [Fact] + [WorkItem(82131, "https://github.com/dotnet/roslyn/pull/82131")] + public Task TestExperimentalApi_PropertyInExperimentalClassAsync() + => VerifyNet80CSharpAdditionalFileFixAsync($$""" + using System.Diagnostics.CodeAnalysis; + + [Experimental("ID1")] + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + {{EnabledModifierCSharp}} int Property { {|{{AddNewApiId}}:get|}; {|{{AddNewApiId}}:set|}; } + } + """, @"", @"", """ + [ID1]C + [ID1]C.C() -> void + [ID1]C.Property.get -> int + [ID1]C.Property.set -> void + """); + [Theory] [InlineData("")] [InlineData("null")] @@ -2835,6 +2941,155 @@ public async Task TestExperimentalApiWithInvalidArgumentAsync(string invalidArgu await test.RunAsync(); } + private const string RequiresUnsafeAttributeSource = """ + namespace System.Runtime.CompilerServices + { + internal sealed class RequiresUnsafeAttribute : Attribute { } + } + """; + + [Fact] + public Task TestRequiresUnsafeApiOnMethodAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [RequiresUnsafe] + {{EnabledModifierCSharp}} void {|{{AddNewApiId}}:M|}() { } + } + """, @"", @"", """ + C + C.C() -> void + [RequiresUnsafe]C.M() -> void + """); + + [Fact] + public Task TestRequiresUnsafeApiOnExternMethodAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + {{EnabledModifierCSharp}} extern void {|{{AddNewApiId}}:M1|}() { } + [RequiresUnsafe] + {{EnabledModifierCSharp}} extern void {|{{AddNewApiId}}:M2|}() { } + } + """, @"", @"", """ + C + C.C() -> void + extern C.M1() -> void + [RequiresUnsafe]extern C.M2() -> void + """); + + [Fact] + public Task TestRequiresUnsafeApiOnPropertyAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [RequiresUnsafe] + {{EnabledModifierCSharp}} int Property { {|{{AddNewApiId}}:get|}; {|{{AddNewApiId}}:set|}; } + } + """, @"", @"", """ + C + C.C() -> void + [RequiresUnsafe]C.Property.get -> int + [RequiresUnsafe]C.Property.set -> void + """); + + [Fact] + public Task TestRequiresUnsafeApiOnPropertyAccessorsAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + {{EnabledModifierCSharp}} int Property { [RequiresUnsafe] {|{{AddNewApiId}}:get|}; {|{{AddNewApiId}}:set|}; } + } + """, @"", @"", """ + C + C.C() -> void + C.Property.set -> void + [RequiresUnsafe]C.Property.get -> int + """); + + [Fact] + public Task TestRequiresUnsafeApiOnEventAsync() + => VerifyRequiresUnsafeAdditionalFileFixAsync($$""" + using System; + using System.Runtime.CompilerServices; + + {{EnabledModifierCSharp}} class {|{{AddNewApiId}}:{|{{AddNewApiId}}:C|}|} + { + [RequiresUnsafe] + {{EnabledModifierCSharp}} event EventHandler {|{{AddNewApiId}}:MyEvent|}; + } + """, @"", @"", """ + C + C.C() -> void + [RequiresUnsafe]C.MyEvent -> System.EventHandler + """); + + private async Task VerifyRequiresUnsafeAdditionalFileFixAsync(string source, string? shippedApiText, string? oldUnshippedApiText, string newUnshippedApiText) + { + // The RequiresUnsafeAttribute is defined as internal in test source, so we need to provide + // internal API files and include the attribute type entries to satisfy the internal API analyzer. + // We put these entries in the Shipped file so they don't interfere with the Unshipped file diffs. + var internalApiForAttribute = """ + System.Runtime.CompilerServices.RequiresUnsafeAttribute + System.Runtime.CompilerServices.RequiresUnsafeAttribute.RequiresUnsafeAttribute() -> void + """; + + var test = new CSharpCodeFixTest() + { + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, + CompilerDiagnostics = CompilerDiagnostics.None, + }; + + test.TestState.Sources.Add(source); + test.TestState.Sources.Add(RequiresUnsafeAttributeSource); + + if (IsInternalTest) + { + // For internal tests, ShippedFileName/UnshippedFileName are InternalAPI files. + // Put the RequiresUnsafeAttribute entries in the shipped file. + test.TestState.AdditionalFiles.Add((ShippedFileName, internalApiForAttribute)); + test.TestState.AdditionalFiles.Add((UnshippedFileName, oldUnshippedApiText ?? "")); + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.PublicShippedFileName, "")); + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.PublicUnshippedFileName, "")); + + test.FixedState.Sources.Add(source); + test.FixedState.Sources.Add(RequiresUnsafeAttributeSource); + test.FixedState.AdditionalFiles.Add((ShippedFileName, internalApiForAttribute)); + test.FixedState.AdditionalFiles.Add((UnshippedFileName, newUnshippedApiText)); + test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.PublicShippedFileName, "")); + test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.PublicUnshippedFileName, "")); + } + else + { + // For public tests, provide internal API files with the attribute entries pre-populated. + if (shippedApiText != null) + test.TestState.AdditionalFiles.Add((ShippedFileName, shippedApiText)); + if (oldUnshippedApiText != null) + test.TestState.AdditionalFiles.Add((UnshippedFileName, oldUnshippedApiText)); + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.InternalShippedFileName, internalApiForAttribute)); + test.TestState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.InternalUnshippedFileName, "")); + + test.FixedState.Sources.Add(source); + test.FixedState.Sources.Add(RequiresUnsafeAttributeSource); + test.FixedState.AdditionalFiles.Add((ShippedFileName, shippedApiText ?? string.Empty)); + test.FixedState.AdditionalFiles.Add((UnshippedFileName, newUnshippedApiText)); + test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.InternalShippedFileName, internalApiForAttribute)); + test.FixedState.AdditionalFiles.Add((DeclarePublicApiAnalyzer.InternalUnshippedFileName, "")); + } + + test.DisabledDiagnostics.AddRange(DisabledDiagnostics); + + await test.RunAsync(); + } + #endregion } } diff --git a/src/RoslynAnalyzers/Roslyn.Diagnostics.Analyzers/Roslyn.Diagnostics.Analyzers.sarif b/src/RoslynAnalyzers/Roslyn.Diagnostics.Analyzers/Roslyn.Diagnostics.Analyzers.sarif index db3b25ecdb3b..cb057bc000b0 100644 --- a/src/RoslynAnalyzers/Roslyn.Diagnostics.Analyzers/Roslyn.Diagnostics.Analyzers.sarif +++ b/src/RoslynAnalyzers/Roslyn.Diagnostics.Analyzers/Roslyn.Diagnostics.Analyzers.sarif @@ -5,7 +5,7 @@ { "tool": { "name": "Roslyn.Diagnostics.Analyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -157,7 +157,7 @@ { "tool": { "name": "Roslyn.Diagnostics.CSharp.Analyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -300,7 +300,7 @@ { "tool": { "name": "Roslyn.Diagnostics.VisualBasic.Analyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { diff --git a/src/RoslynAnalyzers/Text.Analyzers/Text.Analyzers.sarif b/src/RoslynAnalyzers/Text.Analyzers/Text.Analyzers.sarif index 8d53897acdb2..64e786ddadad 100644 --- a/src/RoslynAnalyzers/Text.Analyzers/Text.Analyzers.sarif +++ b/src/RoslynAnalyzers/Text.Analyzers/Text.Analyzers.sarif @@ -5,7 +5,7 @@ { "tool": { "name": "Humanizer", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -14,7 +14,7 @@ { "tool": { "name": "Text.Analyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -86,7 +86,7 @@ { "tool": { "name": "Text.CSharp.Analyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { @@ -95,7 +95,7 @@ { "tool": { "name": "Text.VisualBasic.Analyzers", - "version": "5.4.0", + "version": "5.7.0", "language": "en-US" }, "rules": { diff --git a/src/RoslynAnalyzers/Tools/GenerateAnalyzerNuspec/GenerateAnalyzerNuspec.csproj b/src/RoslynAnalyzers/Tools/GenerateAnalyzerNuspec/GenerateAnalyzerNuspec.csproj index 071bdc74b76f..3cbe69d3696a 100644 --- a/src/RoslynAnalyzers/Tools/GenerateAnalyzerNuspec/GenerateAnalyzerNuspec.csproj +++ b/src/RoslynAnalyzers/Tools/GenerateAnalyzerNuspec/GenerateAnalyzerNuspec.csproj @@ -2,6 +2,8 @@ Exe $(NetRoslyn) + + false true false diff --git a/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFiles/GenerateDocumentationAndConfigFiles.csproj b/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFiles/GenerateDocumentationAndConfigFiles.csproj index b8953b75143c..15b4fb9dd8da 100644 --- a/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFiles/GenerateDocumentationAndConfigFiles.csproj +++ b/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFiles/GenerateDocumentationAndConfigFiles.csproj @@ -2,6 +2,8 @@ Exe $(NetRoslyn) + + false true false true diff --git a/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFiles/Program.cs b/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFiles/Program.cs index 721aef45d491..30f674704249 100644 --- a/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFiles/Program.cs +++ b/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFiles/Program.cs @@ -516,7 +516,9 @@ string getCompilerVisibleProperties() + + diff --git a/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFilesForBrokenRuntime/GenerateDocumentationAndConfigFilesForBrokenRuntime.csproj b/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFilesForBrokenRuntime/GenerateDocumentationAndConfigFilesForBrokenRuntime.csproj index fd1727c0af84..8ff4b82dc8eb 100644 --- a/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFilesForBrokenRuntime/GenerateDocumentationAndConfigFilesForBrokenRuntime.csproj +++ b/src/RoslynAnalyzers/Tools/GenerateDocumentationAndConfigFilesForBrokenRuntime/GenerateDocumentationAndConfigFilesForBrokenRuntime.csproj @@ -2,6 +2,8 @@ Exe $(NetRoslyn) + + false true false true diff --git a/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj b/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj index 61bfdc1c685f..7f1d66ac346d 100644 --- a/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj +++ b/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj @@ -7,9 +7,9 @@ <_NuGetRepackAssembly Condition="'$(MSBuildRuntimeType)' == 'Core'">$(NuGetPackageRoot)microsoft.dotnet.nugetrepack.tasks\$(MicrosoftDotnetNuGetRepackTasksVersion)\tools\netcoreapp2.1\Microsoft.DotNet.NuGetRepack.Tasks.dll - - - + + + diff --git a/src/Test/PdbUtilities/EditAndContinue/EditAndContinueTest.cs b/src/Test/PdbUtilities/EditAndContinue/EditAndContinueTest.cs index a9f419d74cd2..2b76b95f5865 100644 --- a/src/Test/PdbUtilities/EditAndContinue/EditAndContinueTest.cs +++ b/src/Test/PdbUtilities/EditAndContinue/EditAndContinueTest.cs @@ -208,7 +208,7 @@ public void Dispose() { // If the test has thrown an exception, or the test host has crashed, we don't want to assert here // or we'll hide it, so we need to do this dodgy looking thing. - var isInException = Marshal.GetExceptionPointers() != IntPtr.Zero; + var isInException = ExecutionConditionUtil.IsMonoCore ? false : Marshal.GetExceptionPointers() != IntPtr.Zero; Assert.True(isInException || _hasVerified, "No Verify call since the last AddGeneration call."); foreach (var disposable in _disposables) diff --git a/src/Tools/AnalyzerRunner/AnalyzerRunner.csproj b/src/Tools/AnalyzerRunner/AnalyzerRunner.csproj index 94d6c0dfba65..257c6f168d48 100644 --- a/src/Tools/AnalyzerRunner/AnalyzerRunner.csproj +++ b/src/Tools/AnalyzerRunner/AnalyzerRunner.csproj @@ -8,7 +8,6 @@ false true - $(NoWarn);NETSDK1206 diff --git a/src/Tools/BuildValidator/BuildValidator.csproj b/src/Tools/BuildValidator/BuildValidator.csproj index 24eefed6e66e..2c00bf175b2c 100644 --- a/src/Tools/BuildValidator/BuildValidator.csproj +++ b/src/Tools/BuildValidator/BuildValidator.csproj @@ -10,6 +10,7 @@ true false true + true AnyCPU diff --git a/src/Tools/ExternalAccess/Razor/EditorFeatures/InternalAPI.Unshipped.txt b/src/Tools/ExternalAccess/Razor/EditorFeatures/InternalAPI.Unshipped.txt index 7c839d0782e1..8ef7217a72d7 100644 --- a/src/Tools/ExternalAccess/Razor/EditorFeatures/InternalAPI.Unshipped.txt +++ b/src/Tools/ExternalAccess/Razor/EditorFeatures/InternalAPI.Unshipped.txt @@ -101,8 +101,6 @@ Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorClassificationOptionsWrapper.Ra Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorClassificationOptionsWrapper.RazorClassificationOptionsWrapper(Microsoft.CodeAnalysis.Classification.ClassificationOptions underlyingObject) -> void Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorClassifierAccessor Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpFormattingInteractionService -Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpInterceptionMiddleLayerWrapper -Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpInterceptionMiddleLayerWrapper.RazorCSharpInterceptionMiddleLayerWrapper(Microsoft.CodeAnalysis.ExternalAccess.Razor.IRazorCSharpInterceptionMiddleLayer! razorCSharpInterceptionMiddleLayer) -> void Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpProximityExpressionResolverService Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDocumentExcerptServiceWrapper Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDocumentExcerptServiceWrapper.RazorDocumentExcerptServiceWrapper(Microsoft.CodeAnalysis.ExternalAccess.Razor.IRazorDocumentExcerptServiceImplementation! impl) -> void @@ -121,11 +119,6 @@ Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDynamicFileInfo.RazorDynamicFil Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDynamicFileInfo.SourceCodeKind.get -> Microsoft.CodeAnalysis.SourceCodeKind Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDynamicFileInfo.TextLoader.get -> Microsoft.CodeAnalysis.TextLoader! Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDynamicFileInfo.ToUpdatedDocumentInfo(Microsoft.CodeAnalysis.DocumentInfo! existingDocumentInfo) -> Microsoft.CodeAnalysis.DocumentInfo! -Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDynamicFileInfoProviderWrapper -Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDynamicFileInfoProviderWrapper.GetDynamicFileInfoAsync(Microsoft.CodeAnalysis.ProjectId! projectId, string? projectFilePath, string! filePath, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDynamicFileInfoProviderWrapper.RazorDynamicFileInfoProviderWrapper(System.Lazy! innerDynamicFileInfoProvider) -> void -Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDynamicFileInfoProviderWrapper.RemoveDynamicFileInfoAsync(Microsoft.CodeAnalysis.ProjectId! projectId, string? projectFilePath, string! filePath, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDynamicFileInfoProviderWrapper.Updated -> System.EventHandler? Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorExcerptMode Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorExcerptMode.SingleLine = 0 -> Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorExcerptMode Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorExcerptMode.Tooltip = 1 -> Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorExcerptMode @@ -232,9 +225,6 @@ override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostLanguageC override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostLanguageClient.ShowNotificationOnInitializeFailed.get -> bool override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorCohostRequestContext.GetHashCode() -> int override Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.RazorTextDocumentIdentifier.GetHashCode() -> int -override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpInterceptionMiddleLayerWrapper.CanHandle(string! methodName) -> bool -override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpInterceptionMiddleLayerWrapper.HandleNotificationAsync(string! methodName, Newtonsoft.Json.Linq.JToken! methodParam, System.Func! sendNotification) -> System.Threading.Tasks.Task! -override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorCSharpInterceptionMiddleLayerWrapper.HandleRequestAsync(string! methodName, Newtonsoft.Json.Linq.JToken! methodParam, System.Func!>! sendRequest) -> System.Threading.Tasks.Task! override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorDocumentPropertiesServiceWrapper.DiagnosticsLspClientName.get -> string? override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorIndentationOptions.GetHashCode() -> int override Microsoft.CodeAnalysis.ExternalAccess.Razor.RazorSpanMappingServiceWrapper.GetMappedTextChangesAsync(Microsoft.CodeAnalysis.Document! oldDocument, Microsoft.CodeAnalysis.Document! newDocument, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! diff --git a/src/Tools/ExternalAccess/Razor/EditorFeatures/RazorCSharpInterceptionMiddleLayer.cs b/src/Tools/ExternalAccess/Razor/EditorFeatures/RazorCSharpInterceptionMiddleLayer.cs deleted file mode 100644 index 6f3b4d9546a8..000000000000 --- a/src/Tools/ExternalAccess/Razor/EditorFeatures/RazorCSharpInterceptionMiddleLayer.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Composition; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Editor.Implementation.LanguageClient; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.ExternalAccess.Razor -{ - [Export(typeof(AbstractLanguageClientMiddleLayer))] - [Shared] - internal class RazorCSharpInterceptionMiddleLayerWrapper : AbstractLanguageClientMiddleLayer - { - private readonly IRazorCSharpInterceptionMiddleLayer _razorCSharpInterceptionMiddleLayer; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RazorCSharpInterceptionMiddleLayerWrapper(IRazorCSharpInterceptionMiddleLayer razorCSharpInterceptionMiddleLayer) - { - _razorCSharpInterceptionMiddleLayer = razorCSharpInterceptionMiddleLayer; - } - - public override bool CanHandle(string methodName) - => _razorCSharpInterceptionMiddleLayer.CanHandle(methodName); - - public override Task HandleNotificationAsync(string methodName, JsonElement methodParam, Func sendNotification) - { - // Razor only ever looks at the method name, so it is safe to pass null for all the Newtonsoft JToken params. - return _razorCSharpInterceptionMiddleLayer.HandleNotificationAsync(methodName, null!, null!); - } - - public override Task HandleRequestAsync(string methodName, JsonElement methodParam, Func> sendRequest) - { - // Razor only implements a middlelayer for semantic tokens refresh, which is a notification. - // Cohosting makes all this unnecessary, so keeping this as minimal as possible until then. - throw new NotImplementedException(); - } - } -} diff --git a/src/Tools/ExternalAccess/Razor/EditorFeatures/RazorDynamicFileInfoProviderWrapper.cs b/src/Tools/ExternalAccess/Razor/EditorFeatures/RazorDynamicFileInfoProviderWrapper.cs deleted file mode 100644 index 5ec8df1492b7..000000000000 --- a/src/Tools/ExternalAccess/Razor/EditorFeatures/RazorDynamicFileInfoProviderWrapper.cs +++ /dev/null @@ -1,95 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Composition; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.ExternalAccess.Razor -{ - [Shared] - [ExportMetadata("Extensions", new string[] { "cshtml", "razor", })] - [Export(typeof(IDynamicFileInfoProvider))] - internal class RazorDynamicFileInfoProviderWrapper : IDynamicFileInfoProvider - { - private readonly Lazy _innerDynamicFileInfoProvider; - private readonly object _attachLock = new object(); - private bool _attached; - - public event EventHandler? Updated; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RazorDynamicFileInfoProviderWrapper( - Lazy innerDynamicFileInfoProvider) - { - _innerDynamicFileInfoProvider = innerDynamicFileInfoProvider ?? throw new ArgumentNullException(nameof(innerDynamicFileInfoProvider)); - } - - public async Task GetDynamicFileInfoAsync(ProjectId projectId, string? projectFilePath, string filePath, CancellationToken cancellationToken) - { - // We lazily attach to the dynamic file info provider in order to ensure that Razor assemblies are not loaded in non-Razor contexts. - if (!EnsureAttached()) - { - // Razor workload is not installed. Can't build dynamic file infos for any Razor files. - return null; - } - - var result = await _innerDynamicFileInfoProvider.Value.GetDynamicFileInfoAsync(projectId, projectFilePath, filePath, cancellationToken).ConfigureAwait(false); - // This might not be a file/project Razor is interested in - if (result is null) - { - return null; - } - - var serviceProvider = new RazorDocumentServiceProviderWrapper(result.DocumentServiceProvider); - var razorDocumentPropertiesService = result.DocumentServiceProvider.GetService(); - var designTimeOnly = razorDocumentPropertiesService?.DesignTimeOnly ?? false; - var dynamicFileInfo = new DynamicFileInfo(result.FilePath, result.SourceCodeKind, result.TextLoader, designTimeOnly, serviceProvider); - - return dynamicFileInfo; - } - - public Task RemoveDynamicFileInfoAsync(ProjectId projectId, string? projectFilePath, string filePath, CancellationToken cancellationToken) - { - if (_innerDynamicFileInfoProvider == null) - { - // Razor workload is not installed. Can't remove any dynamic file infos. - return Task.CompletedTask; - } - - return _innerDynamicFileInfoProvider.Value.RemoveDynamicFileInfoAsync(projectId, projectFilePath, filePath, cancellationToken); - } - - private void InnerDynamicFileInfoProvider_Updated(object? sender, string e) - { - Updated?.Invoke(this, e); - } - - private bool EnsureAttached() - { - lock (_attachLock) - { - if (_attached) - { - return true; - } - - if (_innerDynamicFileInfoProvider.Value == null) - { - // Razor workload not installed, can't attach. - return false; - } - - _attached = true; - _innerDynamicFileInfoProvider.Value.Updated += InnerDynamicFileInfoProvider_Updated; - - return true; - } - } - } -} diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/Constants.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/Constants.cs index dd849edb980a..d2005a4b1ba1 100644 --- a/src/Tools/ExternalAccess/Razor/Features/Cohost/Constants.cs +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/Constants.cs @@ -4,6 +4,7 @@ using System; using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.RemoveUnnecessaryImports; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; @@ -13,4 +14,10 @@ internal static class Constants // These UI contexts are provided by Razor, so must match https://github.com/dotnet/razor/blob/main/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorConstants.cs public static readonly Guid RazorCohostingUIContext = new Guid("6d5b86dc-6b8a-483b-ae30-098a3c7d6774"); + + internal static class DiagnosticIds + { + public const string RemoveUnnecessaryImportsFixable = RemoveUnnecessaryImportsConstants.DiagnosticFixableId; + public const string IDE0005_gen = RemoveUnnecessaryImportsConstants.IDE0005_gen; + } } diff --git a/src/Tools/ExternalAccess/Razor/Features/Microsoft.CodeAnalysis.ExternalAccess.Razor.Features.csproj b/src/Tools/ExternalAccess/Razor/Features/Microsoft.CodeAnalysis.ExternalAccess.Razor.Features.csproj index bc5712e05a6d..7d79c5d28066 100644 --- a/src/Tools/ExternalAccess/Razor/Features/Microsoft.CodeAnalysis.ExternalAccess.Razor.Features.csproj +++ b/src/Tools/ExternalAccess/Razor/Features/Microsoft.CodeAnalysis.ExternalAccess.Razor.Features.csproj @@ -3,7 +3,7 @@ Microsoft.CodeAnalysis.ExternalAccess.Razor.Features - $(NetVSCode);$(NetVS);netstandard2.0 + $(NetVSShared);netstandard2.0 enable diff --git a/src/Tools/GenerateRulesMissingDocumentation/GenerateRulesMissingDocumentation.csproj b/src/Tools/GenerateRulesMissingDocumentation/GenerateRulesMissingDocumentation.csproj index fd9ab276f0a7..77e3b14cdf33 100644 --- a/src/Tools/GenerateRulesMissingDocumentation/GenerateRulesMissingDocumentation.csproj +++ b/src/Tools/GenerateRulesMissingDocumentation/GenerateRulesMissingDocumentation.csproj @@ -3,6 +3,8 @@ Exe $(NetRoslyn) + + false enable enable diff --git a/src/Tools/GenerateRulesMissingDocumentation/Program.cs b/src/Tools/GenerateRulesMissingDocumentation/Program.cs index 5a45ae514096..6be95c8839f4 100644 --- a/src/Tools/GenerateRulesMissingDocumentation/Program.cs +++ b/src/Tools/GenerateRulesMissingDocumentation/Program.cs @@ -100,7 +100,7 @@ await checkHelpLinkAsync(helpLinkUri).ConfigureAwait(false)) File.WriteAllText(fileWithPath, builder.ToString()); } -// NOTE: Network errors (timeouts and 5xx status codes) are not considered failures. +// NOTE: Network errors (timeouts, rate limits, and 5xx status codes) are not considered failures. async Task checkHelpLinkAsync(string helpLink) { try @@ -117,7 +117,7 @@ async Task checkHelpLinkAsync(string helpLink) if (!success && response is not null) { Console.WriteLine($"##[warning]Failed to check '{helpLink}': {response.StatusCode}"); - if ((int)response.StatusCode >= 500) + if ((int)response.StatusCode is (int)HttpStatusCode.TooManyRequests or >= 500) { return true; } diff --git a/src/Tools/IdeBenchmarks/IdeBenchmarks.csproj b/src/Tools/IdeBenchmarks/IdeBenchmarks.csproj index fe23579d497f..2c7a531bc23d 100644 --- a/src/Tools/IdeBenchmarks/IdeBenchmarks.csproj +++ b/src/Tools/IdeBenchmarks/IdeBenchmarks.csproj @@ -13,11 +13,12 @@ true + true diff --git a/src/Tools/IdeBenchmarks/Lsp/LspSourceGeneratorBenchmarks.cs b/src/Tools/IdeBenchmarks/Lsp/LspSourceGeneratorBenchmarks.cs new file mode 100644 index 000000000000..36f301e66475 --- /dev/null +++ b/src/Tools/IdeBenchmarks/Lsp/LspSourceGeneratorBenchmarks.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.SourceGenerators; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Test.Utilities; +using Xunit; +using LSP = Roslyn.LanguageServer.Protocol; +using SumType = Roslyn.LanguageServer.Protocol.SumType; + +namespace IdeBenchmarks.Lsp +{ + [MemoryDiagnoser] + [GcServer(true)] + public class LspSourceGeneratorBenchmarks : AbstractLanguageServerProtocolTests + { + private readonly UseExportProviderAttribute _useExportProviderAttribute = new UseExportProviderAttribute(); + + private TestLspServer? _testServer; + + /// + /// Number of typing edits to perform per benchmark iteration. + /// + [Params(50, 1000)] + public int TypingIterations { get; set; } + + [Params("Automatic", "Balanced")] + public string ExecutionPreference { get; set; } = "Automatic"; + + public LspSourceGeneratorBenchmarks() : base(null) + { + } + + private SourceGeneratorExecutionPreference GetExecutionPreference() + => ExecutionPreference == "Balanced" + ? SourceGeneratorExecutionPreference.Balanced + : SourceGeneratorExecutionPreference.Automatic; + + [GlobalSetup] + public void GlobalSetup() + { + } + + [IterationSetup] + public void IterationSetup() => LoadSolutionAsync().Wait(); + + private async Task LoadSolutionAsync() + { + _useExportProviderAttribute.Before(null); + + // Typing extends the partial method name: partial void M() → Ma() → Maa() → ... + // Each keystroke changes the method name, forcing the generator to update its output. + var markup = """ + public partial class C + { + public int F => _field; + partial void M{|typing:|}(); + } + """; + + _testServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace: true); + + // Set the execution preference. + var configService = _testServer.TestWorkspace.ExportProvider.GetExportedValue(); + configService.Options = new WorkspaceConfigurationOptions(SourceGeneratorExecution: GetExecutionPreference()); + + var document = _testServer.GetCurrentSolution().Projects.Single().Documents.Single(); + + // Add a source generator that produces a partial class with a backing field + // when it detects "public partial class C" in the compilation. + var generator = new BenchmarkSourceGenerator(); + + _testServer.TestWorkspace.OnAnalyzerReferenceAdded( + document.Project.Id, + new TestGeneratorReference(generator)); + + await _testServer.WaitForSourceGeneratorsAsync(); + + // Open the document in LSP so edits flow through the server. + await _testServer.OpenDocumentAsync(document.GetURI()); + } + + [Benchmark] + public async Task TypeAndWaitForSourceGenerators() + { + var testServer = _testServer!; + + var typingLocation = testServer.GetLocations("typing").Single(); + var documentUri = typingLocation.DocumentUri; + var typingLine = typingLocation.Range.Start.Line; + var typingColumn = typingLocation.Range.Start.Character; + + // Simulate typing one character at a time and waiting for source generators after each keystroke. + // After each wait, pull diagnostics for the document (mirrors what VS Code does on every change). + for (var i = 0; i < TypingIterations; i++) + { + await testServer.InsertTextAsync(documentUri, (typingLine, typingColumn + i, "a")); + await testServer.WaitForSourceGeneratorsAsync(); + + await testServer.WaitForDiagnosticsAsync(); + await testServer.ExecuteRequestAsync( + LSP.Methods.TextDocumentDiagnosticName, + new LSP.DocumentDiagnosticParams + { + TextDocument = new LSP.TextDocumentIdentifier { DocumentUri = documentUri }, + }, + CancellationToken.None); + } + + // After all typing, refresh source generators (simulates save / explicit refresh in balanced mode). + await testServer.RefreshSourceGeneratorsAsync(forceRegeneration: false); + + // Request the source generated document text to verify the generator ran. + var sourceGeneratedDocuments = await testServer.GetCurrentSolution().Projects.Single().GetSourceGeneratedDocumentsAsync(); + Assert.NotEmpty(sourceGeneratedDocuments); + + var sgIdentity = sourceGeneratedDocuments.Single().Identity; + var sgUri = SourceGeneratedDocumentUri.Create(sgIdentity); + var sgText = await testServer.ExecuteRequestAsync( + SourceGeneratedDocumentGetTextHandler.MethodName, + new SourceGeneratorGetTextParams(new LSP.TextDocumentIdentifier { DocumentUri = sgUri }, ResultId: null), + CancellationToken.None); + + AssertEx.NotNull(sgText); + Assert.Contains("public partial class C", sgText.Text); + } + + [IterationCleanup] + public void Cleanup() + { + if (_testServer is not null) + { + _testServer.DisposeAsync().AsTask().Wait(); + } + + _useExportProviderAttribute.After(null); + } + + /// + /// A source generator that looks for type "C" in the compilation, generates a backing + /// field (_field), and produces implementations for any partial methods it finds. + /// As the user types and the partial method name grows (M → Ma → Maa …), the generated + /// output changes on every keystroke. + /// +#pragma warning disable RS1042 // test only generator + private sealed class BenchmarkSourceGenerator : ISourceGenerator +#pragma warning restore RS1042 // test only generator + { + public void Initialize(GeneratorInitializationContext context) + { + } + + public void Execute(GeneratorExecutionContext context) + { + var typeC = context.Compilation.GetTypeByMetadataName("C"); + if (typeC is null) + return; + + var sb = new StringBuilder(); + sb.AppendLine("public partial class C"); + sb.AppendLine("{"); + sb.AppendLine(" private readonly int _field = 1;"); + + // Generate implementations for every partial method definition on C. + foreach (var member in typeC.GetMembers()) + { + if (member is IMethodSymbol { IsPartialDefinition: true } method) + { + sb.AppendLine($" partial void {method.Name}() {{ }}"); + } + } + + sb.AppendLine("}"); + + context.AddSource("GeneratedPartial", SourceText.From(sb.ToString(), Encoding.UTF8)); + } + } + } +} diff --git a/src/Tools/IdeCoreBenchmarks/IdeCoreBenchmarks.csproj b/src/Tools/IdeCoreBenchmarks/IdeCoreBenchmarks.csproj index 6ba03793f013..d5b5dddad56f 100644 --- a/src/Tools/IdeCoreBenchmarks/IdeCoreBenchmarks.csproj +++ b/src/Tools/IdeCoreBenchmarks/IdeCoreBenchmarks.csproj @@ -13,11 +13,12 @@ true + true diff --git a/src/Tools/IdeCoreBenchmarks/NavigateToFuzzyPreFilterBenchmarks.cs b/src/Tools/IdeCoreBenchmarks/NavigateToFuzzyPreFilterBenchmarks.cs new file mode 100644 index 000000000000..04176cce9d60 --- /dev/null +++ b/src/Tools/IdeCoreBenchmarks/NavigateToFuzzyPreFilterBenchmarks.cs @@ -0,0 +1,174 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Text; +using BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.PatternMatching; +using Roslyn.Utilities; + +namespace IdeCoreBenchmarks; + +/// +/// Benchmarks specifically for the fuzzy pre-filter improvements: the corrected length check +/// and the q-gram bigram count check. Demonstrates the scenario where many symbols share the +/// same length as the pattern, making the length-only check produce false positives that the +/// bigram check rejects. +/// +[MemoryDiagnoser] +public class NavigateToFuzzyPreFilterBenchmarks +{ + private NavigateToSearchIndex _sameLengthIndex = null!; + private NavigateToSearchIndex _variedLengthIndex = null!; + + [GlobalSetup] + public void GlobalSetup() + { + _sameLengthIndex = CreateSameLengthIndex(); + _variedLengthIndex = CreateVariedLengthIndex(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Same-length index: 1000 6-character symbols, all different from "XyzWvq" + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// 1000 symbols each with length 6, using varied CamelCase names. The pattern "XyzWvq" also + /// has length 6 — so the length check always passes. But the bigram check can discriminate: + /// none of these symbols share bigrams "xy","yz","zw","wv","vq". + /// + private static NavigateToSearchIndex CreateSameLengthIndex() + { + var stringTable = new StringTable(); + var infos = new DeclaredSymbolInfo[1000]; + var sb = new StringBuilder(); + + for (var i = 0; i < 1000; i++) + { + sb.Clear(); + var c1 = (char)('A' + (i % 26)); + var c2 = (char)('A' + ((i / 26) % 26)); + sb.Append(c1).Append("ab").Append(c2).Append("cd"); + infos[i] = MakeInfo(stringTable, sb.ToString(), "Test.Ns"); + } + + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos.ToImmutableArray()); + } + + /// + /// Same 1000 symbols but with lengths ranging from 4 to 12, so the length check itself + /// provides meaningful filtering for some patterns. + /// + private static NavigateToSearchIndex CreateVariedLengthIndex() + { + var stringTable = new StringTable(); + var infos = new DeclaredSymbolInfo[1000]; + var sb = new StringBuilder(); + + for (var i = 0; i < 1000; i++) + { + sb.Clear(); + var len = 4 + (i % 9); + sb.Append((char)('A' + (i % 26))); + for (var j = 1; j < len; j++) + sb.Append((char)('a' + ((i * 3 + j * 7) % 26))); + infos[i] = MakeInfo(stringTable, sb.ToString(), "Test.Ns"); + } + + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos.ToImmutableArray()); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Length check: passes for same-length index, provides filtering for varied + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// "XyzWvq" (length 6) against 1000 length-6 symbols → length check always passes. + /// This is the false-positive scenario that motivated the bigram check. + /// + [Benchmark(Description = "SameLen: LengthCheck pass (false positive)")] + public bool SameLength_LengthCheck_Pass() + => _sameLengthIndex.GetTestAccessor().LengthCheckPasses("XyzWvq"); + + /// + /// "XyzWvq" (length 6) against varied-length symbols → length check may or may not pass + /// depending on whether any symbol has length 4–8. + /// + [Benchmark(Description = "VariedLen: LengthCheck pass")] + public bool VariedLength_LengthCheck_Pass() + => _variedLengthIndex.GetTestAccessor().LengthCheckPasses("XyzWvq"); + + // ═══════════════════════════════════════════════════════════════════════════ + // Bigram count check: provides strong filtering even for same-length symbols + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// "XyzWvq" (length 6) against 1000 length-6 symbols → bigram check rejects because + /// bigrams "xy","yz","zw","wv","vq" are not stored in any of the "AabBcd" style names. + /// This is the key improvement: where length check produces a false positive, the bigram + /// check correctly rejects. + /// + [Benchmark(Description = "SameLen: BigramCheck reject (true negative!)")] + public bool SameLength_BigramCheck_Reject() + => _sameLengthIndex.GetTestAccessor().BigramCountCheckPasses("XyzWvq"); + + /// + /// "AabBcd" (length 6) against same-length index → bigram check passes because these + /// exact bigrams are stored. + /// + [Benchmark(Description = "SameLen: BigramCheck pass (true positive)")] + public bool SameLength_BigramCheck_Pass() + => _sameLengthIndex.GetTestAccessor().BigramCountCheckPasses("AabBcd"); + + // ═══════════════════════════════════════════════════════════════════════════ + // Combined: CouldContainNavigateToMatch with fuzzy result + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// Pattern "XyzWvq": hump check fails (X not stored), not all-lowercase so trigram fails, + /// length check passes but bigram check fails → result is false. The bigram check saved + /// us from a futile fuzzy matching scan. + /// + [Benchmark(Description = "SameLen: Combined reject (bigram saves)")] + public bool SameLength_Combined_Reject() + => _sameLengthIndex.CouldContainNavigateToMatch("XyzWvq", null) != PatternMatcherKind.None; + + /// + /// Pattern "AabBxx": hump check passes (A,B stored), length check passes, bigram check + /// passes → result is true with fuzzy enabled. + /// + [Benchmark(Description = "SameLen: Combined pass")] + public bool SameLength_Combined_Pass() + => _sameLengthIndex.CouldContainNavigateToMatch("AabBxx", null) != PatternMatcherKind.None; + + // ═══════════════════════════════════════════════════════════════════════════ + // Longer patterns: bigram filtering gets stronger + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// Length 8 pattern (k=2, min_shared=3): needs ≥ 3 of 7 bigrams. More selective. + /// + [Benchmark(Description = "SameLen: BigramCheck reject len=8")] + public bool SameLength_BigramCheck_Reject_Len8() + => _sameLengthIndex.GetTestAccessor().BigramCountCheckPasses("XyzWvqRs"); + + /// + /// Length 10 pattern (k=2, min_shared=5): needs ≥ 5 of 9 bigrams. Very selective. + /// + [Benchmark(Description = "VariedLen: BigramCheck reject len=10")] + public bool VariedLength_BigramCheck_Reject_Len10() + => _variedLengthIndex.GetTestAccessor().BigramCountCheckPasses("XyzWvqRsTu"); + + // ═══════════════════════════════════════════════════════════════════════════ + + private static DeclaredSymbolInfo MakeInfo(StringTable stringTable, string name, string container) + => DeclaredSymbolInfo.Create( + stringTable, name, nameSuffix: null, containerDisplayName: null, + fullyQualifiedContainerName: container, + isPartial: false, hasAttributes: false, + DeclaredSymbolInfoKind.Method, Accessibility.Public, + default, ImmutableArray.Empty); +} diff --git a/src/Tools/IdeCoreBenchmarks/NavigateToPreFilterBenchmarks.cs b/src/Tools/IdeCoreBenchmarks/NavigateToPreFilterBenchmarks.cs new file mode 100644 index 000000000000..c0fdb1f1eee7 --- /dev/null +++ b/src/Tools/IdeCoreBenchmarks/NavigateToPreFilterBenchmarks.cs @@ -0,0 +1,408 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.PatternMatching; +using Roslyn.Utilities; + +namespace IdeCoreBenchmarks; + +/// +/// Measures NavigateTo pre-filter performance across different index sizes and matching algorithms. +/// For each index, benchmarks test both hit (filter says "probably matches") and miss (filter rejects). +/// The full-scan baseline shows the cost we avoid by rejecting early. +/// +[MemoryDiagnoser] +public class NavigateToPreFilterBenchmarks +{ + private NavigateToSearchIndex _realistic = null!; + private NavigateToSearchIndex _stressAll = null!; + private NavigateToSearchIndex _stressHump = null!; + private NavigateToSearchIndex _stressPrefix = null!; + private NavigateToSearchIndex _stressTrigram = null!; + private NavigateToSearchIndex _stressContainer = null!; + private string[] _realisticSymbolNames = null!; + + [GlobalSetup] + public void GlobalSetup() + { + _realistic = CreateRealisticIndex(out _realisticSymbolNames); + _stressAll = CreateStressAllIndex(); + _stressHump = CreateStressHumpSetIndex(); + _stressPrefix = CreateStressHumpPrefixIndex(); + _stressTrigram = CreateStressTrigramIndex(); + _stressContainer = CreateStressContainerIndex(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Baseline: full PatternMatcher scan (the cost we're avoiding) + // ═══════════════════════════════════════════════════════════════════════════ + + [Benchmark(Baseline = true, Description = "FullScan (10k symbols, no pre-filter)")] + public int FullScan() + { + using var matcher = PatternMatcher.CreatePatternMatcher( + "FoNa", includeMatchedSpans: false, PatternMatcherKind.Standard | PatternMatcherKind.Fuzzy); + var count = 0; + foreach (var name in _realisticSymbolNames) + { + using var matches = TemporaryArray.Empty; + if (matcher.AddMatches(name, ref matches.AsRef())) + count++; + } + + return count; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // Realistic index: 10k symbols from hard-coded CamelCase names + 5 containers + // ═══════════════════════════════════════════════════════════════════════════ + + private static NavigateToSearchIndex CreateRealisticIndex(out string[] symbolNames) + { + var stringTable = new StringTable(); + const int count = 10_000; + var infos = new DeclaredSymbolInfo[count]; + symbolNames = new string[count]; + + // Names like "GetApplicationContext0", "SetDatabaseConnection1", etc. + // Humps: G,A,C / S,D,C / C,S,P / P,X,D / V,U,I / T,D,R / C,H,C / I,C / D,R,M / S,O,G + // Containers: System.Collections.Generic, Microsoft.CodeAnalysis.CSharp, etc. + var namePrefixes = new[] + { + "GetApplicationContext", + "SetDatabaseConnection", + "CreateServiceProvider", + "ParseXmlDocument", + "ValidateUserInput", + "TransformDataRecord", + "CalculateHashCode", + "InitializeComponent", + "DisposeResourceManager", + "SerializeObjectGraph", + }; + + var containers = new[] + { + "System.Collections.Generic", + "Microsoft.CodeAnalysis.CSharp", + "Azure.Storage.Blobs", + "Newtonsoft.Json.Linq", + "System.Threading.Tasks", + }; + + for (var i = 0; i < count; i++) + { + var name = $"{namePrefixes[i % namePrefixes.Length]}{i}"; + symbolNames[i] = name; + infos[i] = MakeInfo(stringTable, name, containers[i % containers.Length]); + } + + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos.ToImmutableArray()); + } + + // "FoNa" → humps F,N → bigram "FN" not in hump set (no name starts with F+N humps) → miss. + [Benchmark(Description = "Realistic: CamelCase miss")] + public bool Realistic_CamelCase_Miss() + => _realistic.CouldContainNavigateToMatch("FoNa", null) != PatternMatcherKind.None; + + // "GetApp" → humps G,A → bigram "GA" is stored from "GetApplicationContext" → hit. + [Benchmark(Description = "Realistic: CamelCase hit")] + public bool Realistic_CamelCase_Hit() + => _realistic.CouldContainNavigateToMatch("GetApp", null) != PatternMatcherKind.None; + + // "getapp" → all-lowercase → DP splits "get"+"app", both are hump prefixes of + // "Get" and "Application" → hit. + [Benchmark(Description = "Realistic: lowercase hit (hump-prefix DP)")] + public bool Realistic_Lowercase_Hit() + => _realistic.CouldContainNavigateToMatch("getapp", null) != PatternMatcherKind.None; + + // "foozy" → all-lowercase → DP can't split into any stored hump prefixes (no name starts + // with "foo...") → hump miss. Trigrams "foo","ooz","ozy" not stored either → miss. + [Benchmark(Description = "Realistic: lowercase miss (hump-prefix DP)")] + public bool Realistic_Lowercase_Miss() + => _realistic.CouldContainNavigateToMatch("foozy", null) != PatternMatcherKind.None; + + // "context" → all-lowercase, len 7 → trigrams "con","ont","nte","tex","ext" all stored + // from "GetApplicationContext" → hit. + [Benchmark(Description = "Realistic: lowercase trigram hit")] + public bool Realistic_Lowercase_Trigram_Hit() + => _realistic.CouldContainNavigateToMatch("context", null) != PatternMatcherKind.None; + + // "Context" → mixed-case → MixedCaseHumpCheckPasses → single hump 'C' in hump set → hit. + [Benchmark(Description = "Realistic: CamelCase trigram hit")] + public bool Realistic_CamelCase_Trigram_Hit() + => _realistic.CouldContainNavigateToMatch("Context", null) != PatternMatcherKind.None; + + // "zqjxw" → all-lowercase, len 5 → trigrams "zqj","qjx","jxw" none stored → miss. + [Benchmark(Description = "Realistic: lowercase trigram miss")] + public bool Realistic_Lowercase_Trigram_Miss() + => _realistic.CouldContainNavigateToMatch("zqjxw", null) != PatternMatcherKind.None; + + // "Zqjxw" → mixed-case → MixedCaseHumpCheckPasses → single hump 'Z' not in hump set → miss. + [Benchmark(Description = "Realistic: CamelCase trigram miss")] + public bool Realistic_CamelCase_Trigram_Miss() + => _realistic.CouldContainNavigateToMatch("Zqjxw", null) != PatternMatcherKind.None; + + // "GetApp" hits name check. "System.Collections" → humps S,C match container chars + // from "System.Collections.Generic" → hit. + [Benchmark(Description = "Realistic: container hit")] + public bool Realistic_Container_Hit() + => _realistic.CouldContainNavigateToMatch("GetApp", "System.Collections") != PatternMatcherKind.None; + + // "GetApp" hits name check. "Zebra.Unknown" → hump Z not in any container → miss. + [Benchmark(Description = "Realistic: container miss")] + public bool Realistic_Container_Miss() + => _realistic.CouldContainNavigateToMatch("GetApp", "Zebra.Unknown") != PatternMatcherKind.None; + + // "@getapp" → strip leading '@' → "getapp" → all-lowercase → hump-prefix DP hit. + [Benchmark(Description = "Realistic: lowercase verbatim hit")] + public bool Realistic_Lowercase_Verbatim_Hit() + => _realistic.CouldContainNavigateToMatch("@getapp", null) != PatternMatcherKind.None; + + // "@GetApp" → strip leading '@' → "GetApp" → CamelCase → bigram "GA" hit. + [Benchmark(Description = "Realistic: CamelCase verbatim hit")] + public bool Realistic_CamelCase_Verbatim_Hit() + => _realistic.CouldContainNavigateToMatch("@GetApp", null) != PatternMatcherKind.None; + + // "get context" → split at space → both all-lowercase words checked via hump-prefix DP. + [Benchmark(Description = "Realistic: lowercase multi-word hit")] + public bool Realistic_Lowercase_MultiWord_Hit() + => _realistic.CouldContainNavigateToMatch("get context", null) != PatternMatcherKind.None; + + // "Get Context" → split at space → both CamelCase words checked via hump set. + [Benchmark(Description = "Realistic: CamelCase multi-word hit")] + public bool Realistic_CamelCase_MultiWord_Hit() + => _realistic.CouldContainNavigateToMatch("Get Context", null) != PatternMatcherKind.None; + + // ═══════════════════════════════════════════════════════════════════════════ + // Stress-all index: every structure saturated simultaneously + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// Saturates every structure at once: all 676 CamelCase bigrams, 500 long-hump names for prefix + /// bloom, 500 long-word names for trigram bloom, and containers spanning all 26 uppercase initials. + /// + private static NavigateToSearchIndex CreateStressAllIndex() + { + var stringTable = new StringTable(); + var infos = new List(); + var sb = new StringBuilder(); + + // 676 two-hump names covering every (A..Z, A..Z) bigram pair. + for (var c1 = 'A'; c1 <= 'Z'; c1++) + { + for (var c2 = 'A'; c2 <= 'Z'; c2++) + { + sb.Clear().Append(c1).Append("aa").Append(c2).Append("bb"); + infos.Add(MakeInfo(stringTable, sb.ToString(), "All.Stress")); + } + } + + // 500 single-hump names with long unique lowercase tails → many hump prefixes. + for (var i = 0; i < 500; i++) + { + sb.Clear().Append((char)('A' + (i % 26))); + for (var j = 0; j < 20; j++) + sb.Append((char)('a' + ((i * 7 + j * 13) % 26))); + infos.Add(MakeInfo(stringTable, sb.ToString(), "All.Stress")); + } + + // 500 single-word names with long varied sequences → many trigrams. + for (var i = 0; i < 500; i++) + { + sb.Clear().Append((char)('A' + (i % 26))); + for (var j = 0; j < 25; j++) + sb.Append((char)('a' + ((i * 11 + j * 17) % 26))); + infos.Add(MakeInfo(stringTable, sb.ToString(), "All.Stress")); + } + + // Containers spanning all 26 uppercase initials. + for (var c = 'A'; c <= 'Z'; c++) + infos.Add(MakeInfo(stringTable, "Method" + c, $"{c}lpha.{(char)('A' + (c - 'A' + 13) % 26)}eta")); + + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos.ToImmutableArray()); + } + + // "AaBb" → humps A,B → bigram "AB" is stored (all 676 bigrams present) → hit. + [Benchmark(Description = "StressAll: hit")] + public bool StressAll_Hit() + => _stressAll.CouldContainNavigateToMatch("AaBb", null) != PatternMatcherKind.None; + + // "FoNa" → humps F,N → bigram "FN" is stored (all bigrams present), so hump check hits. + // However the name "FoNa" doesn't actually exist — this tests the cost of a false positive + // from the hump set (it passes but the actual PatternMatcher would reject later). + [Benchmark(Description = "StressAll: miss")] + public bool StressAll_Miss() + => _stressAll.CouldContainNavigateToMatch("FoNa", null) != PatternMatcherKind.None; + + // ═══════════════════════════════════════════════════════════════════════════ + // Stress _humpSet: large frozen set of bigrams/chars + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// 5,000 names with 3 humps each, systematically varying initials across A-Z. + /// Names like "AabAcdAef", "BabAcdAef", ..., covering many unique bigram pairs. + /// Word-parts kept short (3 chars each) to minimize trigram/prefix noise. + /// + private static NavigateToSearchIndex CreateStressHumpSetIndex() + { + var stringTable = new StringTable(); + var infos = new DeclaredSymbolInfo[5000]; + var sb = new StringBuilder(); + + for (var i = 0; i < 5000; i++) + { + sb.Clear(); + var h1 = (char)('A' + (i % 26)); + var h2 = (char)('A' + ((i / 26) % 26)); + var h3 = (char)('A' + ((i / 676) % 26)); + sb.Append(h1).Append("ab"); + sb.Append(h2).Append("cd"); + sb.Append(h3).Append("ef"); + infos[i] = MakeInfo(stringTable, sb.ToString(), "X.Y"); + } + + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos.ToImmutableArray()); + } + + // "AaCd" → humps A,C → bigram "AC" is stored (name "AabCcdXef" exists) → hit. + [Benchmark(Description = "StressHumpSet: CamelCase hit")] + public bool StressHumpSet_Hit() + => _stressHump.CouldContainNavigateToMatch("AaCd", null) != PatternMatcherKind.None; + + // "FoNa" → humps F,N → bigram "FN" not stored (h2 varies with h1, not all pairs covered). + // No names have both F and N as adjacent hump initials → miss. + [Benchmark(Description = "StressHumpSet: CamelCase miss")] + public bool StressHumpSet_Miss() + => _stressHump.CouldContainNavigateToMatch("FoNa", null) != PatternMatcherKind.None; + + // ═══════════════════════════════════════════════════════════════════════════ + // Stress _humpPrefixFilter: dense bloom of lowercased hump prefixes + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// 2,000 names each with a single long hump (20 lowercase chars after the initial). + /// Each name produces ~20 unique prefixes, flooding the bloom filter with ~40,000 + /// distinct prefix strings. Character sequences use (i*7 + j*13) % 26 for variety. + /// + private static NavigateToSearchIndex CreateStressHumpPrefixIndex() + { + var stringTable = new StringTable(); + var infos = new DeclaredSymbolInfo[2000]; + var sb = new StringBuilder(); + + for (var i = 0; i < 2000; i++) + { + sb.Clear().Append((char)('A' + (i % 26))); + for (var j = 0; j < 20; j++) + sb.Append((char)('a' + ((i * 7 + j * 13) % 26))); + infos[i] = MakeInfo(stringTable, sb.ToString(), "X.Y"); + } + + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos.ToImmutableArray()); + } + + // "aahub" → all-lowercase → DP tries to split into hump prefixes. Names starting with 'A' + // have varied prefixes; "a","aa","aah",... likely stored in the dense bloom → hit. + [Benchmark(Description = "StressHumpPrefix: lowercase hit")] + public bool StressHumpPrefix_Hit() + => _stressPrefix.CouldContainNavigateToMatch("aahub", null) != PatternMatcherKind.None; + + // "zzzzqqqq" → all-lowercase → DP tries prefixes "z","zz","zzz","zzzz",... + // No name has these as hump prefixes (no repeated-z sequences stored) → miss. + [Benchmark(Description = "StressHumpPrefix: lowercase miss")] + public bool StressHumpPrefix_Miss() + => _stressPrefix.CouldContainNavigateToMatch("zzzzqqqq", null) != PatternMatcherKind.None; + + // ═══════════════════════════════════════════════════════════════════════════ + // Stress _trigramFilter: dense bloom of 3-char sliding windows + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// 2,000 names each with a long single word-part (25 lowercase chars after the initial). + /// Each produces ~23 trigrams, flooding the bloom filter with ~46,000 distinct trigram + /// strings. Character sequences use (i*11 + j*17) % 26 for variety. + /// + private static NavigateToSearchIndex CreateStressTrigramIndex() + { + var stringTable = new StringTable(); + var infos = new DeclaredSymbolInfo[2000]; + var sb = new StringBuilder(); + + for (var i = 0; i < 2000; i++) + { + sb.Clear().Append((char)('A' + (i % 26))); + for (var j = 0; j < 25; j++) + sb.Append((char)('a' + ((i * 11 + j * 17) % 26))); + infos[i] = MakeInfo(stringTable, sb.ToString(), "X.Y"); + } + + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos.ToImmutableArray()); + } + + // "aahub" → all-lowercase, len 5 → trigrams "aah","ahu","hub" checked against dense bloom. + // With 46k trigrams stored, common 3-char sequences are likely present → hit. + [Benchmark(Description = "StressTrigram: lowercase hit")] + public bool StressTrigram_Hit() + => _stressTrigram.CouldContainNavigateToMatch("aahub", null) != PatternMatcherKind.None; + + // "zqxjw" → all-lowercase, len 5 → trigrams "zqx","qxj","xjw" are rare 3-char sequences + // unlikely to appear even in a dense bloom → miss. + [Benchmark(Description = "StressTrigram: lowercase miss")] + public bool StressTrigram_Miss() + => _stressTrigram.CouldContainNavigateToMatch("zqxjw", null) != PatternMatcherKind.None; + + // ═══════════════════════════════════════════════════════════════════════════ + // Stress _containerCharSet: all 26 A-Z initials in frozen char set + // ═══════════════════════════════════════════════════════════════════════════ + + /// + /// 26 symbols with containers whose hump initials span all of A-Z (e.g., + /// "Aoo.Far.Naz", "Boo.Gar.Oaz", ...), making the frozen char set as full as possible. + /// All symbols share name "SimpleMethod". + /// + private static NavigateToSearchIndex CreateStressContainerIndex() + { + var stringTable = new StringTable(); + var infos = new DeclaredSymbolInfo[26]; + + for (var i = 0; i < 26; i++) + { + var c = (char)('A' + i); + infos[i] = MakeInfo(stringTable, "SimpleMethod", + $"{c}oo.{(char)('A' + (i + 5) % 26)}ar.{(char)('A' + (i + 13) % 26)}az"); + } + + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos.ToImmutableArray()); + } + + // "Simple" hits name check (hump 'S' matches "SimpleMethod"). "Foo.Bar" → humps F,B → + // both present in full A-Z container char set → hit. + [Benchmark(Description = "StressContainer: container hit")] + public bool StressContainer_Hit() + => _stressContainer.CouldContainNavigateToMatch("Simple", "Foo.Bar") != PatternMatcherKind.None; + + // "Simple" hits name check. "0Invalid.1Bad" → hump initials '0','1' are digits, + // not in the A-Z container char set → miss. + [Benchmark(Description = "StressContainer: container miss")] + public bool StressContainer_Miss() + => _stressContainer.CouldContainNavigateToMatch("Simple", "0Invalid.1Bad") != PatternMatcherKind.None; + + // ═══════════════════════════════════════════════════════════════════════════ + + private static DeclaredSymbolInfo MakeInfo(StringTable stringTable, string name, string container) + => DeclaredSymbolInfo.Create( + stringTable, name, nameSuffix: null, containerDisplayName: null, + fullyQualifiedContainerName: container, + isPartial: false, hasAttributes: false, + DeclaredSymbolInfoKind.Method, Accessibility.Public, + default, ImmutableArray.Empty); +} diff --git a/src/Tools/IdeCoreBenchmarks/NavigateToRegexPreFilterBenchmarks.cs b/src/Tools/IdeCoreBenchmarks/NavigateToRegexPreFilterBenchmarks.cs new file mode 100644 index 000000000000..5e84f1041fed --- /dev/null +++ b/src/Tools/IdeCoreBenchmarks/NavigateToRegexPreFilterBenchmarks.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Text; +using BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.NavigateTo; +using Microsoft.CodeAnalysis.PatternMatching; +using Roslyn.Utilities; + +namespace IdeCoreBenchmarks; + +/// +/// Benchmarks for regex-based NavigateTo pre-filtering: query compilation from a regex pattern, +/// pre-filter evaluation against a document's bigram index, and full regex matching. +/// +[MemoryDiagnoser] +public class NavigateToRegexPreFilterBenchmarks +{ + private NavigateToSearchIndex _index = null!; + private RegexQuery _simpleQuery = null!; + private RegexQuery _alternationQuery = null!; + private RegexQuery _complexQuery = null!; + private RegexQuery _noMatchQuery = null!; + + [GlobalSetup] + public void GlobalSetup() + { + _index = CreateIndex(); + _simpleQuery = RegexQueryCompiler.Compile("ReadLine")!; + _alternationQuery = RegexQueryCompiler.Compile("(Read|Write)Line")!; + _complexQuery = RegexQueryCompiler.Compile("(Get|Set)(Value|Item)s?")!; + _noMatchQuery = RegexQueryCompiler.Compile("Xyz.*Wvq")!; + } + + private static NavigateToSearchIndex CreateIndex() + { + var stringTable = new StringTable(); + var infos = new DeclaredSymbolInfo[1000]; + var names = new[] + { + "ReadLine", "WriteLine", "ReadKey", "WriteBuffer", "StreamReader", + "StreamWriter", "GetValue", "SetValue", "GetItem", "SetItem", + "ToString", "GetHashCode", "Equals", "CompareTo", "Dispose", + "Initialize", "Configure", "Execute", "Validate", "Transform", + }; + + for (var i = 0; i < 1000; i++) + { + var name = names[i % names.Length] + (i / names.Length); + infos[i] = DeclaredSymbolInfo.Create( + stringTable, name, nameSuffix: null, containerDisplayName: null, + fullyQualifiedContainerName: "", isPartial: false, hasAttributes: false, + DeclaredSymbolInfoKind.Method, Accessibility.Public, + default, ImmutableArray.Empty); + } + + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos.ToImmutableArray()); + } + + [Benchmark(Description = "Compile: simple literal")] + public object? CompileSimple() => RegexQueryCompiler.Compile("ReadLine"); + + [Benchmark(Description = "Compile: alternation")] + public object? CompileAlternation() => RegexQueryCompiler.Compile("(Read|Write)Line"); + + [Benchmark(Description = "Compile: complex")] + public object? CompileComplex() => RegexQueryCompiler.Compile("(Get|Set)(Value|Item)s?"); + + [Benchmark(Description = "PreFilter: simple literal (match)")] + public bool PreFilterSimple() => _index.GetTestAccessor().RegexQueryCheckPasses(_simpleQuery); + + [Benchmark(Description = "PreFilter: alternation (match)")] + public bool PreFilterAlternation() => _index.GetTestAccessor().RegexQueryCheckPasses(_alternationQuery); + + [Benchmark(Description = "PreFilter: complex (match)")] + public bool PreFilterComplex() => _index.GetTestAccessor().RegexQueryCheckPasses(_complexQuery); + + [Benchmark(Description = "PreFilter: no match")] + public bool PreFilterNoMatch() => _index.GetTestAccessor().RegexQueryCheckPasses(_noMatchQuery); + + [Benchmark(Description = "IsRegexPattern: plain text")] + public bool DetectPlainText() => RegexPatternDetector.IsRegexPattern("ReadLine"); + + [Benchmark(Description = "IsRegexPattern: regex")] + public bool DetectRegex() => RegexPatternDetector.IsRegexPattern("(Read|Write)Line"); +} diff --git a/src/Tools/PrepareTests/TestDiscovery.cs b/src/Tools/PrepareTests/TestDiscovery.cs index 57f95aa05424..f9b74a8bc90c 100644 --- a/src/Tools/PrepareTests/TestDiscovery.cs +++ b/src/Tools/PrepareTests/TestDiscovery.cs @@ -113,9 +113,20 @@ bool ShouldInclude(string path) { if (isUnix) { + var dirName = Path.GetFileName(Path.GetDirectoryName(path)); + // Our unix build will build net framework dlls for multi-targeted projects. // These are not valid testing on unix and discovery will throw if we try. - return Path.GetFileName(Path.GetDirectoryName(path)) != "net472"; + if (dirName is "net472") + { + return false; + } + + // TFMs with OS-specific suffixes (e.g. net10-windows) should not run on unix. + if (dirName is not null && dirName.EndsWith("-windows", StringComparison.Ordinal)) + { + return false; + } } return true; diff --git a/src/Tools/Replay/Replay.csproj b/src/Tools/Replay/Replay.csproj index a51b16f6197d..c14d7a386861 100644 --- a/src/Tools/Replay/Replay.csproj +++ b/src/Tools/Replay/Replay.csproj @@ -5,6 +5,14 @@ true false $(DefineConstants);ROSLYN_NO_INDEXRANGE + + true + true diff --git a/src/Tools/SemanticSearch/Extensions/Microsoft.CodeAnalysis.SemanticSearch.Extensions.csproj b/src/Tools/SemanticSearch/Extensions/Microsoft.CodeAnalysis.SemanticSearch.Extensions.csproj index 0be85833704c..0abfcb79e7e9 100644 --- a/src/Tools/SemanticSearch/Extensions/Microsoft.CodeAnalysis.SemanticSearch.Extensions.csproj +++ b/src/Tools/SemanticSearch/Extensions/Microsoft.CodeAnalysis.SemanticSearch.Extensions.csproj @@ -3,7 +3,7 @@ Library - $(NetVSShared) + $(NetRoslynAll) true diff --git a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.CSharp.txt b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.CSharp.txt index 805dd4ded842..9055b65658f7 100644 --- a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.CSharp.txt +++ b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.CSharp.txt @@ -1171,6 +1171,7 @@ Microsoft.CodeAnalysis.CSharp.Conversion.get_IsThrow Microsoft.CodeAnalysis.CSharp.Conversion.get_IsTupleConversion Microsoft.CodeAnalysis.CSharp.Conversion.get_IsTupleLiteralConversion Microsoft.CodeAnalysis.CSharp.Conversion.get_IsUnboxing +Microsoft.CodeAnalysis.CSharp.Conversion.get_IsUnion Microsoft.CodeAnalysis.CSharp.Conversion.get_IsUserDefined Microsoft.CodeAnalysis.CSharp.Conversion.get_MethodSymbol Microsoft.CodeAnalysis.CSharp.Conversion.op_Equality(Microsoft.CodeAnalysis.CSharp.Conversion,Microsoft.CodeAnalysis.CSharp.Conversion) @@ -5441,6 +5442,7 @@ Microsoft.CodeAnalysis.CSharp.SyntaxFactory.StackAllocArrayCreationExpression(Mi Microsoft.CodeAnalysis.CSharp.SyntaxFactory.StackAllocArrayCreationExpression(Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax,Microsoft.CodeAnalysis.CSharp.Syntax.InitializerExpressionSyntax) Microsoft.CodeAnalysis.CSharp.SyntaxFactory.StackAllocArrayCreationExpression(Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax) Microsoft.CodeAnalysis.CSharp.SyntaxFactory.StackAllocArrayCreationExpression(Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.CSharp.Syntax.TypeSyntax,Microsoft.CodeAnalysis.CSharp.Syntax.InitializerExpressionSyntax) +Microsoft.CodeAnalysis.CSharp.SyntaxFactory.StructDeclaration(Microsoft.CodeAnalysis.CSharp.SyntaxKind,Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.AttributeListSyntax},Microsoft.CodeAnalysis.SyntaxTokenList,Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax,Microsoft.CodeAnalysis.CSharp.Syntax.ParameterListSyntax,Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax,Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterConstraintClauseSyntax},Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.MemberDeclarationSyntax},Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.SyntaxToken) Microsoft.CodeAnalysis.CSharp.SyntaxFactory.StructDeclaration(Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.AttributeListSyntax},Microsoft.CodeAnalysis.SyntaxTokenList,Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax,Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax,Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterConstraintClauseSyntax},Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.MemberDeclarationSyntax}) Microsoft.CodeAnalysis.CSharp.SyntaxFactory.StructDeclaration(Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.AttributeListSyntax},Microsoft.CodeAnalysis.SyntaxTokenList,Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax,Microsoft.CodeAnalysis.CSharp.Syntax.ParameterListSyntax,Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax,Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterConstraintClauseSyntax},Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.MemberDeclarationSyntax}) Microsoft.CodeAnalysis.CSharp.SyntaxFactory.StructDeclaration(Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.AttributeListSyntax},Microsoft.CodeAnalysis.SyntaxTokenList,Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterListSyntax,Microsoft.CodeAnalysis.CSharp.Syntax.BaseListSyntax,Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.TypeParameterConstraintClauseSyntax},Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.SyntaxList{Microsoft.CodeAnalysis.CSharp.Syntax.MemberDeclarationSyntax},Microsoft.CodeAnalysis.SyntaxToken,Microsoft.CodeAnalysis.SyntaxToken) @@ -6257,6 +6259,8 @@ Microsoft.CodeAnalysis.CSharp.SyntaxKind.UncheckedStatement Microsoft.CodeAnalysis.CSharp.SyntaxKind.UndefDirectiveTrivia Microsoft.CodeAnalysis.CSharp.SyntaxKind.UndefKeyword Microsoft.CodeAnalysis.CSharp.SyntaxKind.UnderscoreToken +Microsoft.CodeAnalysis.CSharp.SyntaxKind.UnionDeclaration +Microsoft.CodeAnalysis.CSharp.SyntaxKind.UnionKeyword Microsoft.CodeAnalysis.CSharp.SyntaxKind.UnknownAccessorDeclaration Microsoft.CodeAnalysis.CSharp.SyntaxKind.UnmanagedKeyword Microsoft.CodeAnalysis.CSharp.SyntaxKind.UnsafeKeyword diff --git a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.txt b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.txt index c264898d2f76..adad2d06e3de 100644 --- a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.txt +++ b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/Microsoft.CodeAnalysis.txt @@ -1946,6 +1946,7 @@ Microsoft.CodeAnalysis.Operations.CommonConversion.get_IsImplicit Microsoft.CodeAnalysis.Operations.CommonConversion.get_IsNullable Microsoft.CodeAnalysis.Operations.CommonConversion.get_IsNumeric Microsoft.CodeAnalysis.Operations.CommonConversion.get_IsReference +Microsoft.CodeAnalysis.Operations.CommonConversion.get_IsUnion Microsoft.CodeAnalysis.Operations.CommonConversion.get_IsUserDefined Microsoft.CodeAnalysis.Operations.CommonConversion.get_MethodSymbol Microsoft.CodeAnalysis.Operations.IAddressOfOperation @@ -3838,6 +3839,8 @@ Microsoft.CodeAnalysis.Text.SourceHashAlgorithm Microsoft.CodeAnalysis.Text.SourceHashAlgorithm.None Microsoft.CodeAnalysis.Text.SourceHashAlgorithm.Sha1 Microsoft.CodeAnalysis.Text.SourceHashAlgorithm.Sha256 +Microsoft.CodeAnalysis.Text.SourceHashAlgorithm.Sha384 +Microsoft.CodeAnalysis.Text.SourceHashAlgorithm.Sha512 Microsoft.CodeAnalysis.Text.SourceHashAlgorithm.value__ Microsoft.CodeAnalysis.Text.SourceText Microsoft.CodeAnalysis.Text.SourceText.#ctor(System.Collections.Immutable.ImmutableArray{System.Byte},Microsoft.CodeAnalysis.Text.SourceHashAlgorithm,Microsoft.CodeAnalysis.Text.SourceTextContainer) @@ -4085,6 +4088,7 @@ Microsoft.CodeAnalysis.WellKnownMemberNames.GetEnumeratorMethodName Microsoft.CodeAnalysis.WellKnownMemberNames.GetResult Microsoft.CodeAnalysis.WellKnownMemberNames.GreaterThanOperatorName Microsoft.CodeAnalysis.WellKnownMemberNames.GreaterThanOrEqualOperatorName +Microsoft.CodeAnalysis.WellKnownMemberNames.HasValuePropertyName Microsoft.CodeAnalysis.WellKnownMemberNames.ImplicitConversionName Microsoft.CodeAnalysis.WellKnownMemberNames.IncrementAssignmentOperatorName Microsoft.CodeAnalysis.WellKnownMemberNames.IncrementOperatorName @@ -4123,6 +4127,7 @@ Microsoft.CodeAnalysis.WellKnownMemberNames.SubtractionOperatorName Microsoft.CodeAnalysis.WellKnownMemberNames.TopLevelStatementsEntryPointMethodName Microsoft.CodeAnalysis.WellKnownMemberNames.TopLevelStatementsEntryPointTypeName Microsoft.CodeAnalysis.WellKnownMemberNames.TrueOperatorName +Microsoft.CodeAnalysis.WellKnownMemberNames.TryGetValueMethodName Microsoft.CodeAnalysis.WellKnownMemberNames.UnaryNegationOperatorName Microsoft.CodeAnalysis.WellKnownMemberNames.UnaryPlusOperatorName Microsoft.CodeAnalysis.WellKnownMemberNames.UnsignedLeftShiftOperatorName diff --git a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Collections.Immutable.txt b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Collections.Immutable.txt index c30cc7062d6e..5fa1c260bdaf 100644 --- a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Collections.Immutable.txt +++ b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Collections.Immutable.txt @@ -6,14 +6,21 @@ System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary``2(System.Collecti System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary``2(System.Collections.Generic.IEnumerable{``0},System.Func{``0,``1},System.Collections.Generic.IEqualityComparer{``1}) System.Collections.Frozen.FrozenDictionary.ToFrozenDictionary``3(System.Collections.Generic.IEnumerable{``0},System.Func{``0,``1},System.Func{``0,``2},System.Collections.Generic.IEqualityComparer{``1}) System.Collections.Frozen.FrozenDictionary`2 +System.Collections.Frozen.FrozenDictionary`2.AlternateLookup`1 +System.Collections.Frozen.FrozenDictionary`2.AlternateLookup`1.ContainsKey(`2) +System.Collections.Frozen.FrozenDictionary`2.AlternateLookup`1.TryGetValue(`2,`1@) +System.Collections.Frozen.FrozenDictionary`2.AlternateLookup`1.get_Dictionary +System.Collections.Frozen.FrozenDictionary`2.AlternateLookup`1.get_Item(`2) System.Collections.Frozen.FrozenDictionary`2.ContainsKey(`0) System.Collections.Frozen.FrozenDictionary`2.CopyTo(System.Collections.Generic.KeyValuePair{`0,`1}[],System.Int32) System.Collections.Frozen.FrozenDictionary`2.CopyTo(System.Span{System.Collections.Generic.KeyValuePair{`0,`1}}) System.Collections.Frozen.FrozenDictionary`2.Enumerator System.Collections.Frozen.FrozenDictionary`2.Enumerator.MoveNext System.Collections.Frozen.FrozenDictionary`2.Enumerator.get_Current +System.Collections.Frozen.FrozenDictionary`2.GetAlternateLookup``1 System.Collections.Frozen.FrozenDictionary`2.GetEnumerator System.Collections.Frozen.FrozenDictionary`2.GetValueRefOrNullRef(`0) +System.Collections.Frozen.FrozenDictionary`2.TryGetAlternateLookup``1(System.Collections.Frozen.FrozenDictionary{`0,`1}.AlternateLookup{``0}@) System.Collections.Frozen.FrozenDictionary`2.TryGetValue(`0,`1@) System.Collections.Frozen.FrozenDictionary`2.get_Comparer System.Collections.Frozen.FrozenDictionary`2.get_Count @@ -26,12 +33,17 @@ System.Collections.Frozen.FrozenSet.Create``1(System.Collections.Generic.IEquali System.Collections.Frozen.FrozenSet.Create``1(System.ReadOnlySpan{``0}) System.Collections.Frozen.FrozenSet.ToFrozenSet``1(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEqualityComparer{``0}) System.Collections.Frozen.FrozenSet`1 +System.Collections.Frozen.FrozenSet`1.AlternateLookup`1 +System.Collections.Frozen.FrozenSet`1.AlternateLookup`1.Contains(`1) +System.Collections.Frozen.FrozenSet`1.AlternateLookup`1.TryGetValue(`1,`0@) +System.Collections.Frozen.FrozenSet`1.AlternateLookup`1.get_Set System.Collections.Frozen.FrozenSet`1.Contains(`0) System.Collections.Frozen.FrozenSet`1.CopyTo(System.Span{`0}) System.Collections.Frozen.FrozenSet`1.CopyTo(`0[],System.Int32) System.Collections.Frozen.FrozenSet`1.Enumerator System.Collections.Frozen.FrozenSet`1.Enumerator.MoveNext System.Collections.Frozen.FrozenSet`1.Enumerator.get_Current +System.Collections.Frozen.FrozenSet`1.GetAlternateLookup``1 System.Collections.Frozen.FrozenSet`1.GetEnumerator System.Collections.Frozen.FrozenSet`1.IsProperSubsetOf(System.Collections.Generic.IEnumerable{`0}) System.Collections.Frozen.FrozenSet`1.IsProperSupersetOf(System.Collections.Generic.IEnumerable{`0}) @@ -39,6 +51,7 @@ System.Collections.Frozen.FrozenSet`1.IsSubsetOf(System.Collections.Generic.IEnu System.Collections.Frozen.FrozenSet`1.IsSupersetOf(System.Collections.Generic.IEnumerable{`0}) System.Collections.Frozen.FrozenSet`1.Overlaps(System.Collections.Generic.IEnumerable{`0}) System.Collections.Frozen.FrozenSet`1.SetEquals(System.Collections.Generic.IEnumerable{`0}) +System.Collections.Frozen.FrozenSet`1.TryGetAlternateLookup``1(System.Collections.Frozen.FrozenSet{`0}.AlternateLookup{``0}@) System.Collections.Frozen.FrozenSet`1.TryGetValue(`0,`0@) System.Collections.Frozen.FrozenSet`1.get_Comparer System.Collections.Frozen.FrozenSet`1.get_Count diff --git a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Collections.txt b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Collections.txt index 4071eee51a37..20ae0f9bda2d 100644 --- a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Collections.txt +++ b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Collections.txt @@ -31,6 +31,7 @@ System.Collections.BitArray.set_Length(System.Int32) System.Collections.Generic.CollectionExtensions System.Collections.Generic.CollectionExtensions.AddRange``1(System.Collections.Generic.List{``0},System.ReadOnlySpan{``0}) System.Collections.Generic.CollectionExtensions.AsReadOnly``1(System.Collections.Generic.IList{``0}) +System.Collections.Generic.CollectionExtensions.AsReadOnly``1(System.Collections.Generic.ISet{``0}) System.Collections.Generic.CollectionExtensions.AsReadOnly``2(System.Collections.Generic.IDictionary{``0,``1}) System.Collections.Generic.CollectionExtensions.CopyTo``1(System.Collections.Generic.List{``0},System.Span{``0}) System.Collections.Generic.CollectionExtensions.GetValueOrDefault``2(System.Collections.Generic.IReadOnlyDictionary{``0,``1},``0) @@ -54,6 +55,16 @@ System.Collections.Generic.Dictionary`2.#ctor(System.Int32) System.Collections.Generic.Dictionary`2.#ctor(System.Int32,System.Collections.Generic.IEqualityComparer{`0}) System.Collections.Generic.Dictionary`2.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) System.Collections.Generic.Dictionary`2.Add(`0,`1) +System.Collections.Generic.Dictionary`2.AlternateLookup`1 +System.Collections.Generic.Dictionary`2.AlternateLookup`1.ContainsKey(`2) +System.Collections.Generic.Dictionary`2.AlternateLookup`1.Remove(`2) +System.Collections.Generic.Dictionary`2.AlternateLookup`1.Remove(`2,`0@,`1@) +System.Collections.Generic.Dictionary`2.AlternateLookup`1.TryAdd(`2,`1) +System.Collections.Generic.Dictionary`2.AlternateLookup`1.TryGetValue(`2,`0@,`1@) +System.Collections.Generic.Dictionary`2.AlternateLookup`1.TryGetValue(`2,`1@) +System.Collections.Generic.Dictionary`2.AlternateLookup`1.get_Dictionary +System.Collections.Generic.Dictionary`2.AlternateLookup`1.get_Item(`2) +System.Collections.Generic.Dictionary`2.AlternateLookup`1.set_Item(`2,`1) System.Collections.Generic.Dictionary`2.Clear System.Collections.Generic.Dictionary`2.ContainsKey(`0) System.Collections.Generic.Dictionary`2.ContainsValue(`1) @@ -62,6 +73,7 @@ System.Collections.Generic.Dictionary`2.Enumerator System.Collections.Generic.Dictionary`2.Enumerator.Dispose System.Collections.Generic.Dictionary`2.Enumerator.MoveNext System.Collections.Generic.Dictionary`2.Enumerator.get_Current +System.Collections.Generic.Dictionary`2.GetAlternateLookup``1 System.Collections.Generic.Dictionary`2.GetEnumerator System.Collections.Generic.Dictionary`2.GetObjectData(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) System.Collections.Generic.Dictionary`2.KeyCollection @@ -80,6 +92,7 @@ System.Collections.Generic.Dictionary`2.Remove(`0,`1@) System.Collections.Generic.Dictionary`2.TrimExcess System.Collections.Generic.Dictionary`2.TrimExcess(System.Int32) System.Collections.Generic.Dictionary`2.TryAdd(`0,`1) +System.Collections.Generic.Dictionary`2.TryGetAlternateLookup``1(System.Collections.Generic.Dictionary{`0,`1}.AlternateLookup{``0}@) System.Collections.Generic.Dictionary`2.TryGetValue(`0,`1@) System.Collections.Generic.Dictionary`2.ValueCollection System.Collections.Generic.Dictionary`2.ValueCollection.#ctor(System.Collections.Generic.Dictionary{`0,`1}) @@ -90,6 +103,7 @@ System.Collections.Generic.Dictionary`2.ValueCollection.Enumerator.MoveNext System.Collections.Generic.Dictionary`2.ValueCollection.Enumerator.get_Current System.Collections.Generic.Dictionary`2.ValueCollection.GetEnumerator System.Collections.Generic.Dictionary`2.ValueCollection.get_Count +System.Collections.Generic.Dictionary`2.get_Capacity System.Collections.Generic.Dictionary`2.get_Comparer System.Collections.Generic.Dictionary`2.get_Count System.Collections.Generic.Dictionary`2.get_Item(`0) @@ -111,6 +125,12 @@ System.Collections.Generic.HashSet`1.#ctor(System.Int32) System.Collections.Generic.HashSet`1.#ctor(System.Int32,System.Collections.Generic.IEqualityComparer{`0}) System.Collections.Generic.HashSet`1.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) System.Collections.Generic.HashSet`1.Add(`0) +System.Collections.Generic.HashSet`1.AlternateLookup`1 +System.Collections.Generic.HashSet`1.AlternateLookup`1.Add(`1) +System.Collections.Generic.HashSet`1.AlternateLookup`1.Contains(`1) +System.Collections.Generic.HashSet`1.AlternateLookup`1.Remove(`1) +System.Collections.Generic.HashSet`1.AlternateLookup`1.TryGetValue(`1,`0@) +System.Collections.Generic.HashSet`1.AlternateLookup`1.get_Set System.Collections.Generic.HashSet`1.Clear System.Collections.Generic.HashSet`1.Contains(`0) System.Collections.Generic.HashSet`1.CopyTo(`0[]) @@ -123,6 +143,7 @@ System.Collections.Generic.HashSet`1.Enumerator.Dispose System.Collections.Generic.HashSet`1.Enumerator.MoveNext System.Collections.Generic.HashSet`1.Enumerator.get_Current System.Collections.Generic.HashSet`1.ExceptWith(System.Collections.Generic.IEnumerable{`0}) +System.Collections.Generic.HashSet`1.GetAlternateLookup``1 System.Collections.Generic.HashSet`1.GetEnumerator System.Collections.Generic.HashSet`1.GetObjectData(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) System.Collections.Generic.HashSet`1.IntersectWith(System.Collections.Generic.IEnumerable{`0}) @@ -137,8 +158,11 @@ System.Collections.Generic.HashSet`1.RemoveWhere(System.Predicate{`0}) System.Collections.Generic.HashSet`1.SetEquals(System.Collections.Generic.IEnumerable{`0}) System.Collections.Generic.HashSet`1.SymmetricExceptWith(System.Collections.Generic.IEnumerable{`0}) System.Collections.Generic.HashSet`1.TrimExcess +System.Collections.Generic.HashSet`1.TrimExcess(System.Int32) +System.Collections.Generic.HashSet`1.TryGetAlternateLookup``1(System.Collections.Generic.HashSet{`0}.AlternateLookup{``0}@) System.Collections.Generic.HashSet`1.TryGetValue(`0,`0@) System.Collections.Generic.HashSet`1.UnionWith(System.Collections.Generic.IEnumerable{`0}) +System.Collections.Generic.HashSet`1.get_Capacity System.Collections.Generic.HashSet`1.get_Comparer System.Collections.Generic.HashSet`1.get_Count System.Collections.Generic.LinkedListNode`1 @@ -241,6 +265,60 @@ System.Collections.Generic.List`1.get_Count System.Collections.Generic.List`1.get_Item(System.Int32) System.Collections.Generic.List`1.set_Capacity(System.Int32) System.Collections.Generic.List`1.set_Item(System.Int32,`0) +System.Collections.Generic.OrderedDictionary`2 +System.Collections.Generic.OrderedDictionary`2.#ctor +System.Collections.Generic.OrderedDictionary`2.#ctor(System.Collections.Generic.IDictionary{`0,`1}) +System.Collections.Generic.OrderedDictionary`2.#ctor(System.Collections.Generic.IDictionary{`0,`1},System.Collections.Generic.IEqualityComparer{`0}) +System.Collections.Generic.OrderedDictionary`2.#ctor(System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{`0,`1}}) +System.Collections.Generic.OrderedDictionary`2.#ctor(System.Collections.Generic.IEnumerable{System.Collections.Generic.KeyValuePair{`0,`1}},System.Collections.Generic.IEqualityComparer{`0}) +System.Collections.Generic.OrderedDictionary`2.#ctor(System.Collections.Generic.IEqualityComparer{`0}) +System.Collections.Generic.OrderedDictionary`2.#ctor(System.Int32) +System.Collections.Generic.OrderedDictionary`2.#ctor(System.Int32,System.Collections.Generic.IEqualityComparer{`0}) +System.Collections.Generic.OrderedDictionary`2.Add(`0,`1) +System.Collections.Generic.OrderedDictionary`2.Clear +System.Collections.Generic.OrderedDictionary`2.ContainsKey(`0) +System.Collections.Generic.OrderedDictionary`2.ContainsValue(`1) +System.Collections.Generic.OrderedDictionary`2.EnsureCapacity(System.Int32) +System.Collections.Generic.OrderedDictionary`2.Enumerator +System.Collections.Generic.OrderedDictionary`2.Enumerator.MoveNext +System.Collections.Generic.OrderedDictionary`2.Enumerator.get_Current +System.Collections.Generic.OrderedDictionary`2.GetAt(System.Int32) +System.Collections.Generic.OrderedDictionary`2.GetEnumerator +System.Collections.Generic.OrderedDictionary`2.IndexOf(`0) +System.Collections.Generic.OrderedDictionary`2.Insert(System.Int32,`0,`1) +System.Collections.Generic.OrderedDictionary`2.KeyCollection +System.Collections.Generic.OrderedDictionary`2.KeyCollection.Contains(`0) +System.Collections.Generic.OrderedDictionary`2.KeyCollection.CopyTo(`0[],System.Int32) +System.Collections.Generic.OrderedDictionary`2.KeyCollection.Enumerator +System.Collections.Generic.OrderedDictionary`2.KeyCollection.Enumerator.MoveNext +System.Collections.Generic.OrderedDictionary`2.KeyCollection.Enumerator.get_Current +System.Collections.Generic.OrderedDictionary`2.KeyCollection.GetEnumerator +System.Collections.Generic.OrderedDictionary`2.KeyCollection.get_Count +System.Collections.Generic.OrderedDictionary`2.Remove(`0) +System.Collections.Generic.OrderedDictionary`2.Remove(`0,`1@) +System.Collections.Generic.OrderedDictionary`2.RemoveAt(System.Int32) +System.Collections.Generic.OrderedDictionary`2.SetAt(System.Int32,`0,`1) +System.Collections.Generic.OrderedDictionary`2.SetAt(System.Int32,`1) +System.Collections.Generic.OrderedDictionary`2.TrimExcess +System.Collections.Generic.OrderedDictionary`2.TrimExcess(System.Int32) +System.Collections.Generic.OrderedDictionary`2.TryAdd(`0,`1) +System.Collections.Generic.OrderedDictionary`2.TryAdd(`0,`1,System.Int32@) +System.Collections.Generic.OrderedDictionary`2.TryGetValue(`0,`1@) +System.Collections.Generic.OrderedDictionary`2.TryGetValue(`0,`1@,System.Int32@) +System.Collections.Generic.OrderedDictionary`2.ValueCollection +System.Collections.Generic.OrderedDictionary`2.ValueCollection.CopyTo(`1[],System.Int32) +System.Collections.Generic.OrderedDictionary`2.ValueCollection.Enumerator +System.Collections.Generic.OrderedDictionary`2.ValueCollection.Enumerator.MoveNext +System.Collections.Generic.OrderedDictionary`2.ValueCollection.Enumerator.get_Current +System.Collections.Generic.OrderedDictionary`2.ValueCollection.GetEnumerator +System.Collections.Generic.OrderedDictionary`2.ValueCollection.get_Count +System.Collections.Generic.OrderedDictionary`2.get_Capacity +System.Collections.Generic.OrderedDictionary`2.get_Comparer +System.Collections.Generic.OrderedDictionary`2.get_Count +System.Collections.Generic.OrderedDictionary`2.get_Item(`0) +System.Collections.Generic.OrderedDictionary`2.get_Keys +System.Collections.Generic.OrderedDictionary`2.get_Values +System.Collections.Generic.OrderedDictionary`2.set_Item(`0,`1) System.Collections.Generic.PriorityQueue`2 System.Collections.Generic.PriorityQueue`2.#ctor System.Collections.Generic.PriorityQueue`2.#ctor(System.Collections.Generic.IComparer{`1}) @@ -257,6 +335,7 @@ System.Collections.Generic.PriorityQueue`2.EnqueueRange(System.Collections.Gener System.Collections.Generic.PriorityQueue`2.EnqueueRange(System.Collections.Generic.IEnumerable{`0},`1) System.Collections.Generic.PriorityQueue`2.EnsureCapacity(System.Int32) System.Collections.Generic.PriorityQueue`2.Peek +System.Collections.Generic.PriorityQueue`2.Remove(`0,`0@,`1@,System.Collections.Generic.IEqualityComparer{`0}) System.Collections.Generic.PriorityQueue`2.TrimExcess System.Collections.Generic.PriorityQueue`2.TryDequeue(`0@,`1@) System.Collections.Generic.PriorityQueue`2.TryPeek(`0@,`1@) @@ -267,6 +346,7 @@ System.Collections.Generic.PriorityQueue`2.UnorderedItemsCollection.Enumerator.M System.Collections.Generic.PriorityQueue`2.UnorderedItemsCollection.Enumerator.get_Current System.Collections.Generic.PriorityQueue`2.UnorderedItemsCollection.GetEnumerator System.Collections.Generic.PriorityQueue`2.UnorderedItemsCollection.get_Count +System.Collections.Generic.PriorityQueue`2.get_Capacity System.Collections.Generic.PriorityQueue`2.get_Comparer System.Collections.Generic.PriorityQueue`2.get_Count System.Collections.Generic.PriorityQueue`2.get_UnorderedItems @@ -288,8 +368,10 @@ System.Collections.Generic.Queue`1.GetEnumerator System.Collections.Generic.Queue`1.Peek System.Collections.Generic.Queue`1.ToArray System.Collections.Generic.Queue`1.TrimExcess +System.Collections.Generic.Queue`1.TrimExcess(System.Int32) System.Collections.Generic.Queue`1.TryDequeue(`0@) System.Collections.Generic.Queue`1.TryPeek(`0@) +System.Collections.Generic.Queue`1.get_Capacity System.Collections.Generic.Queue`1.get_Count System.Collections.Generic.ReferenceEqualityComparer System.Collections.Generic.ReferenceEqualityComparer.Equals(System.Object,System.Object) @@ -424,8 +506,10 @@ System.Collections.Generic.Stack`1.Pop System.Collections.Generic.Stack`1.Push(`0) System.Collections.Generic.Stack`1.ToArray System.Collections.Generic.Stack`1.TrimExcess +System.Collections.Generic.Stack`1.TrimExcess(System.Int32) System.Collections.Generic.Stack`1.TryPeek(`0@) System.Collections.Generic.Stack`1.TryPop(`0@) +System.Collections.Generic.Stack`1.get_Capacity System.Collections.Generic.Stack`1.get_Count System.Collections.StructuralComparisons System.Collections.StructuralComparisons.get_StructuralComparer diff --git a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Linq.txt b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Linq.txt index 6bc2f9932b26..3fc2119f973d 100644 --- a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Linq.txt +++ b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Linq.txt @@ -1,5 +1,7 @@ # Generated, do not update manually System.Linq.Enumerable +System.Linq.Enumerable.AggregateBy``3(System.Collections.Generic.IEnumerable{``0},System.Func{``0,``1},System.Func{``1,``2},System.Func{``2,``0,``2},System.Collections.Generic.IEqualityComparer{``1}) +System.Linq.Enumerable.AggregateBy``3(System.Collections.Generic.IEnumerable{``0},System.Func{``0,``1},``2,System.Func{``2,``0,``2},System.Collections.Generic.IEqualityComparer{``1}) System.Linq.Enumerable.Aggregate``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,``0,``0}) System.Linq.Enumerable.Aggregate``2(System.Collections.Generic.IEnumerable{``0},``1,System.Func{``1,``0,``1}) System.Linq.Enumerable.Aggregate``3(System.Collections.Generic.IEnumerable{``0},``1,System.Func{``1,``0,``1},System.Func{``1,``2}) @@ -33,6 +35,7 @@ System.Linq.Enumerable.Chunk``1(System.Collections.Generic.IEnumerable{``0},Syst System.Linq.Enumerable.Concat``1(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``0}) System.Linq.Enumerable.Contains``1(System.Collections.Generic.IEnumerable{``0},``0) System.Linq.Enumerable.Contains``1(System.Collections.Generic.IEnumerable{``0},``0,System.Collections.Generic.IEqualityComparer{``0}) +System.Linq.Enumerable.CountBy``2(System.Collections.Generic.IEnumerable{``0},System.Func{``0,``1},System.Collections.Generic.IEqualityComparer{``1}) System.Linq.Enumerable.Count``1(System.Collections.Generic.IEnumerable{``0}) System.Linq.Enumerable.Count``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.Boolean}) System.Linq.Enumerable.DefaultIfEmpty``1(System.Collections.Generic.IEnumerable{``0}) @@ -66,6 +69,8 @@ System.Linq.Enumerable.GroupBy``4(System.Collections.Generic.IEnumerable{``0},Sy System.Linq.Enumerable.GroupBy``4(System.Collections.Generic.IEnumerable{``0},System.Func{``0,``1},System.Func{``0,``2},System.Func{``1,System.Collections.Generic.IEnumerable{``2},``3},System.Collections.Generic.IEqualityComparer{``1}) System.Linq.Enumerable.GroupJoin``4(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``1},System.Func{``0,``2},System.Func{``1,``2},System.Func{``0,System.Collections.Generic.IEnumerable{``1},``3}) System.Linq.Enumerable.GroupJoin``4(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``1},System.Func{``0,``2},System.Func{``1,``2},System.Func{``0,System.Collections.Generic.IEnumerable{``1},``3},System.Collections.Generic.IEqualityComparer{``2}) +System.Linq.Enumerable.Index``1(System.Collections.Generic.IEnumerable{``0}) +System.Linq.Enumerable.InfiniteSequence``1(``0,``0) System.Linq.Enumerable.IntersectBy``2(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``1},System.Func{``0,``1}) System.Linq.Enumerable.IntersectBy``2(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``1},System.Func{``0,``1},System.Collections.Generic.IEqualityComparer{``1}) System.Linq.Enumerable.Intersect``1(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``0}) @@ -78,6 +83,8 @@ System.Linq.Enumerable.LastOrDefault``1(System.Collections.Generic.IEnumerable{` System.Linq.Enumerable.LastOrDefault``1(System.Collections.Generic.IEnumerable{``0},``0) System.Linq.Enumerable.Last``1(System.Collections.Generic.IEnumerable{``0}) System.Linq.Enumerable.Last``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.Boolean}) +System.Linq.Enumerable.LeftJoin``4(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``1},System.Func{``0,``2},System.Func{``1,``2},System.Func{``0,``1,``3}) +System.Linq.Enumerable.LeftJoin``4(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``1},System.Func{``0,``2},System.Func{``1,``2},System.Func{``0,``1,``3},System.Collections.Generic.IEqualityComparer{``2}) System.Linq.Enumerable.LongCount``1(System.Collections.Generic.IEnumerable{``0}) System.Linq.Enumerable.LongCount``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.Boolean}) System.Linq.Enumerable.Max(System.Collections.Generic.IEnumerable{System.Decimal}) @@ -143,6 +150,9 @@ System.Linq.Enumerable.Prepend``1(System.Collections.Generic.IEnumerable{``0},`` System.Linq.Enumerable.Range(System.Int32,System.Int32) System.Linq.Enumerable.Repeat``1(``0,System.Int32) System.Linq.Enumerable.Reverse``1(System.Collections.Generic.IEnumerable{``0}) +System.Linq.Enumerable.Reverse``1(``0[]) +System.Linq.Enumerable.RightJoin``4(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``1},System.Func{``0,``2},System.Func{``1,``2},System.Func{``0,``1,``3}) +System.Linq.Enumerable.RightJoin``4(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``1},System.Func{``0,``2},System.Func{``1,``2},System.Func{``0,``1,``3},System.Collections.Generic.IEqualityComparer{``2}) System.Linq.Enumerable.SelectMany``2(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.Collections.Generic.IEnumerable{``1}}) System.Linq.Enumerable.SelectMany``2(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.Int32,System.Collections.Generic.IEnumerable{``1}}) System.Linq.Enumerable.SelectMany``3(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.Collections.Generic.IEnumerable{``1}},System.Func{``0,``1,``2}) @@ -151,6 +161,8 @@ System.Linq.Enumerable.Select``2(System.Collections.Generic.IEnumerable{``0},Sys System.Linq.Enumerable.Select``2(System.Collections.Generic.IEnumerable{``0},System.Func{``0,``1}) System.Linq.Enumerable.SequenceEqual``1(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``0}) System.Linq.Enumerable.SequenceEqual``1(System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEnumerable{``0},System.Collections.Generic.IEqualityComparer{``0}) +System.Linq.Enumerable.Sequence``1(``0,``0,``0) +System.Linq.Enumerable.Shuffle``1(System.Collections.Generic.IEnumerable{``0}) System.Linq.Enumerable.SingleOrDefault``1(System.Collections.Generic.IEnumerable{``0}) System.Linq.Enumerable.SingleOrDefault``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.Boolean}) System.Linq.Enumerable.SingleOrDefault``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.Boolean},``0) diff --git a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Runtime.txt b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Runtime.txt index 58eb505142fd..9de802d369ae 100644 --- a/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Runtime.txt +++ b/src/Tools/SemanticSearch/ReferenceAssemblies/Apis/System.Runtime.txt @@ -202,6 +202,9 @@ System.Array.CreateInstance(System.Type,System.Int32,System.Int32,System.Int32) System.Array.CreateInstance(System.Type,System.Int32[]) System.Array.CreateInstance(System.Type,System.Int32[],System.Int32[]) System.Array.CreateInstance(System.Type,System.Int64[]) +System.Array.CreateInstanceFromArrayType(System.Type,System.Int32) +System.Array.CreateInstanceFromArrayType(System.Type,System.Int32[]) +System.Array.CreateInstanceFromArrayType(System.Type,System.Int32[],System.Int32[]) System.Array.Empty``1 System.Array.Exists``1(``0[],System.Predicate{``0}) System.Array.Fill``1(``0[],``0) @@ -402,10 +405,12 @@ System.BitConverter.GetBytes(System.Boolean) System.BitConverter.GetBytes(System.Char) System.BitConverter.GetBytes(System.Double) System.BitConverter.GetBytes(System.Half) +System.BitConverter.GetBytes(System.Int128) System.BitConverter.GetBytes(System.Int16) System.BitConverter.GetBytes(System.Int32) System.BitConverter.GetBytes(System.Int64) System.BitConverter.GetBytes(System.Single) +System.BitConverter.GetBytes(System.UInt128) System.BitConverter.GetBytes(System.UInt16) System.BitConverter.GetBytes(System.UInt32) System.BitConverter.GetBytes(System.UInt64) @@ -425,6 +430,8 @@ System.BitConverter.ToDouble(System.Byte[],System.Int32) System.BitConverter.ToDouble(System.ReadOnlySpan{System.Byte}) System.BitConverter.ToHalf(System.Byte[],System.Int32) System.BitConverter.ToHalf(System.ReadOnlySpan{System.Byte}) +System.BitConverter.ToInt128(System.Byte[],System.Int32) +System.BitConverter.ToInt128(System.ReadOnlySpan{System.Byte}) System.BitConverter.ToInt16(System.Byte[],System.Int32) System.BitConverter.ToInt16(System.ReadOnlySpan{System.Byte}) System.BitConverter.ToInt32(System.Byte[],System.Int32) @@ -436,6 +443,8 @@ System.BitConverter.ToSingle(System.ReadOnlySpan{System.Byte}) System.BitConverter.ToString(System.Byte[]) System.BitConverter.ToString(System.Byte[],System.Int32) System.BitConverter.ToString(System.Byte[],System.Int32,System.Int32) +System.BitConverter.ToUInt128(System.Byte[],System.Int32) +System.BitConverter.ToUInt128(System.ReadOnlySpan{System.Byte}) System.BitConverter.ToUInt16(System.Byte[],System.Int32) System.BitConverter.ToUInt16(System.ReadOnlySpan{System.Byte}) System.BitConverter.ToUInt32(System.Byte[],System.Int32) @@ -446,10 +455,12 @@ System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.Boolean) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.Char) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.Double) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.Half) +System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.Int128) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.Int16) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.Int32) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.Int64) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.Single) +System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.UInt128) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.UInt16) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.UInt32) System.BitConverter.TryWriteBytes(System.Span{System.Byte},System.UInt64) @@ -518,6 +529,7 @@ System.Buffers.ReadOnlySpanAction`2.Invoke(System.ReadOnlySpan{`0},`1) System.Buffers.SearchValues System.Buffers.SearchValues.Create(System.ReadOnlySpan{System.Byte}) System.Buffers.SearchValues.Create(System.ReadOnlySpan{System.Char}) +System.Buffers.SearchValues.Create(System.ReadOnlySpan{System.String},System.StringComparison) System.Buffers.SearchValues`1 System.Buffers.SearchValues`1.Contains(`0) System.Buffers.SpanAction`2 @@ -536,6 +548,32 @@ System.Buffers.Text.Base64.IsValid(System.ReadOnlySpan{System.Byte}) System.Buffers.Text.Base64.IsValid(System.ReadOnlySpan{System.Byte},System.Int32@) System.Buffers.Text.Base64.IsValid(System.ReadOnlySpan{System.Char}) System.Buffers.Text.Base64.IsValid(System.ReadOnlySpan{System.Char},System.Int32@) +System.Buffers.Text.Base64Url +System.Buffers.Text.Base64Url.DecodeFromChars(System.ReadOnlySpan{System.Char}) +System.Buffers.Text.Base64Url.DecodeFromChars(System.ReadOnlySpan{System.Char},System.Span{System.Byte}) +System.Buffers.Text.Base64Url.DecodeFromChars(System.ReadOnlySpan{System.Char},System.Span{System.Byte},System.Int32@,System.Int32@,System.Boolean) +System.Buffers.Text.Base64Url.DecodeFromUtf8(System.ReadOnlySpan{System.Byte}) +System.Buffers.Text.Base64Url.DecodeFromUtf8(System.ReadOnlySpan{System.Byte},System.Span{System.Byte}) +System.Buffers.Text.Base64Url.DecodeFromUtf8(System.ReadOnlySpan{System.Byte},System.Span{System.Byte},System.Int32@,System.Int32@,System.Boolean) +System.Buffers.Text.Base64Url.DecodeFromUtf8InPlace(System.Span{System.Byte}) +System.Buffers.Text.Base64Url.EncodeToChars(System.ReadOnlySpan{System.Byte}) +System.Buffers.Text.Base64Url.EncodeToChars(System.ReadOnlySpan{System.Byte},System.Span{System.Char}) +System.Buffers.Text.Base64Url.EncodeToChars(System.ReadOnlySpan{System.Byte},System.Span{System.Char},System.Int32@,System.Int32@,System.Boolean) +System.Buffers.Text.Base64Url.EncodeToString(System.ReadOnlySpan{System.Byte}) +System.Buffers.Text.Base64Url.EncodeToUtf8(System.ReadOnlySpan{System.Byte}) +System.Buffers.Text.Base64Url.EncodeToUtf8(System.ReadOnlySpan{System.Byte},System.Span{System.Byte}) +System.Buffers.Text.Base64Url.EncodeToUtf8(System.ReadOnlySpan{System.Byte},System.Span{System.Byte},System.Int32@,System.Int32@,System.Boolean) +System.Buffers.Text.Base64Url.GetEncodedLength(System.Int32) +System.Buffers.Text.Base64Url.GetMaxDecodedLength(System.Int32) +System.Buffers.Text.Base64Url.IsValid(System.ReadOnlySpan{System.Byte}) +System.Buffers.Text.Base64Url.IsValid(System.ReadOnlySpan{System.Byte},System.Int32@) +System.Buffers.Text.Base64Url.IsValid(System.ReadOnlySpan{System.Char}) +System.Buffers.Text.Base64Url.IsValid(System.ReadOnlySpan{System.Char},System.Int32@) +System.Buffers.Text.Base64Url.TryDecodeFromChars(System.ReadOnlySpan{System.Char},System.Span{System.Byte},System.Int32@) +System.Buffers.Text.Base64Url.TryDecodeFromUtf8(System.ReadOnlySpan{System.Byte},System.Span{System.Byte},System.Int32@) +System.Buffers.Text.Base64Url.TryEncodeToChars(System.ReadOnlySpan{System.Byte},System.Span{System.Char},System.Int32@) +System.Buffers.Text.Base64Url.TryEncodeToUtf8(System.ReadOnlySpan{System.Byte},System.Span{System.Byte},System.Int32@) +System.Buffers.Text.Base64Url.TryEncodeToUtf8InPlace(System.Span{System.Byte},System.Int32,System.Int32@) System.Byte System.Byte.Clamp(System.Byte,System.Byte,System.Byte) System.Byte.CompareTo(System.Byte) @@ -734,6 +772,10 @@ System.Collections.DictionaryEntry.get_Key System.Collections.DictionaryEntry.get_Value System.Collections.DictionaryEntry.set_Key(System.Object) System.Collections.DictionaryEntry.set_Value(System.Object) +System.Collections.Generic.IAlternateEqualityComparer`2 +System.Collections.Generic.IAlternateEqualityComparer`2.Create(`0) +System.Collections.Generic.IAlternateEqualityComparer`2.Equals(`0,`1) +System.Collections.Generic.IAlternateEqualityComparer`2.GetHashCode(`0) System.Collections.Generic.IAsyncEnumerable`1 System.Collections.Generic.IAsyncEnumerable`1.GetAsyncEnumerator(System.Threading.CancellationToken) System.Collections.Generic.IAsyncEnumerator`1 @@ -930,6 +972,9 @@ System.Collections.ObjectModel.Collection`1.get_Count System.Collections.ObjectModel.Collection`1.get_Item(System.Int32) System.Collections.ObjectModel.Collection`1.get_Items System.Collections.ObjectModel.Collection`1.set_Item(System.Int32,`0) +System.Collections.ObjectModel.ReadOnlyCollection +System.Collections.ObjectModel.ReadOnlyCollection.CreateCollection``1(System.ReadOnlySpan{``0}) +System.Collections.ObjectModel.ReadOnlyCollection.CreateSet``1(System.ReadOnlySpan{``0}) System.Collections.ObjectModel.ReadOnlyCollection`1 System.Collections.ObjectModel.ReadOnlyCollection`1.#ctor(System.Collections.Generic.IList{`0}) System.Collections.ObjectModel.ReadOnlyCollection`1.Contains(`0) @@ -960,6 +1005,19 @@ System.Collections.ObjectModel.ReadOnlyDictionary`2.get_Empty System.Collections.ObjectModel.ReadOnlyDictionary`2.get_Item(`0) System.Collections.ObjectModel.ReadOnlyDictionary`2.get_Keys System.Collections.ObjectModel.ReadOnlyDictionary`2.get_Values +System.Collections.ObjectModel.ReadOnlySet`1 +System.Collections.ObjectModel.ReadOnlySet`1.#ctor(System.Collections.Generic.ISet{`0}) +System.Collections.ObjectModel.ReadOnlySet`1.Contains(`0) +System.Collections.ObjectModel.ReadOnlySet`1.GetEnumerator +System.Collections.ObjectModel.ReadOnlySet`1.IsProperSubsetOf(System.Collections.Generic.IEnumerable{`0}) +System.Collections.ObjectModel.ReadOnlySet`1.IsProperSupersetOf(System.Collections.Generic.IEnumerable{`0}) +System.Collections.ObjectModel.ReadOnlySet`1.IsSubsetOf(System.Collections.Generic.IEnumerable{`0}) +System.Collections.ObjectModel.ReadOnlySet`1.IsSupersetOf(System.Collections.Generic.IEnumerable{`0}) +System.Collections.ObjectModel.ReadOnlySet`1.Overlaps(System.Collections.Generic.IEnumerable{`0}) +System.Collections.ObjectModel.ReadOnlySet`1.SetEquals(System.Collections.Generic.IEnumerable{`0}) +System.Collections.ObjectModel.ReadOnlySet`1.get_Count +System.Collections.ObjectModel.ReadOnlySet`1.get_Empty +System.Collections.ObjectModel.ReadOnlySet`1.get_Set System.Comparison`1 System.Comparison`1.#ctor(System.Object,System.IntPtr) System.Comparison`1.BeginInvoke(`0,`0,System.AsyncCallback,System.Object) @@ -1023,8 +1081,12 @@ System.Convert.ChangeType(System.Object,System.TypeCode,System.IFormatProvider) System.Convert.DBNull System.Convert.FromBase64CharArray(System.Char[],System.Int32,System.Int32) System.Convert.FromBase64String(System.String) +System.Convert.FromHexString(System.ReadOnlySpan{System.Byte}) +System.Convert.FromHexString(System.ReadOnlySpan{System.Byte},System.Span{System.Byte},System.Int32@,System.Int32@) System.Convert.FromHexString(System.ReadOnlySpan{System.Char}) +System.Convert.FromHexString(System.ReadOnlySpan{System.Char},System.Span{System.Byte},System.Int32@,System.Int32@) System.Convert.FromHexString(System.String) +System.Convert.FromHexString(System.String,System.Span{System.Byte},System.Int32@,System.Int32@) System.Convert.GetTypeCode(System.Object) System.Convert.IsDBNull(System.Object) System.Convert.ToBase64CharArray(System.Byte[],System.Int32,System.Int32,System.Char[],System.Int32) @@ -1146,6 +1208,9 @@ System.Convert.ToDouble(System.UInt64) System.Convert.ToHexString(System.Byte[]) System.Convert.ToHexString(System.Byte[],System.Int32,System.Int32) System.Convert.ToHexString(System.ReadOnlySpan{System.Byte}) +System.Convert.ToHexStringLower(System.Byte[]) +System.Convert.ToHexStringLower(System.Byte[],System.Int32,System.Int32) +System.Convert.ToHexStringLower(System.ReadOnlySpan{System.Byte}) System.Convert.ToInt16(System.Boolean) System.Convert.ToInt16(System.Byte) System.Convert.ToInt16(System.Char) @@ -1336,6 +1401,10 @@ System.Convert.ToUInt64(System.UInt64) System.Convert.TryFromBase64Chars(System.ReadOnlySpan{System.Char},System.Span{System.Byte},System.Int32@) System.Convert.TryFromBase64String(System.String,System.Span{System.Byte},System.Int32@) System.Convert.TryToBase64Chars(System.ReadOnlySpan{System.Byte},System.Span{System.Char},System.Int32@,System.Base64FormattingOptions) +System.Convert.TryToHexString(System.ReadOnlySpan{System.Byte},System.Span{System.Byte},System.Int32@) +System.Convert.TryToHexString(System.ReadOnlySpan{System.Byte},System.Span{System.Char},System.Int32@) +System.Convert.TryToHexStringLower(System.ReadOnlySpan{System.Byte},System.Span{System.Byte},System.Int32@) +System.Convert.TryToHexStringLower(System.ReadOnlySpan{System.Byte},System.Span{System.Char},System.Int32@) System.Converter`2 System.Converter`2.#ctor(System.Object,System.IntPtr) System.Converter`2.BeginInvoke(`0,System.AsyncCallback,System.Object) @@ -1658,6 +1727,8 @@ System.Decimal.Clamp(System.Decimal,System.Decimal,System.Decimal) System.Decimal.Compare(System.Decimal,System.Decimal) System.Decimal.CompareTo(System.Decimal) System.Decimal.CompareTo(System.Object) +System.Decimal.ConvertToIntegerNative``1(System.Decimal) +System.Decimal.ConvertToInteger``1(System.Decimal) System.Decimal.CopySign(System.Decimal,System.Decimal) System.Decimal.CreateChecked``1(``0) System.Decimal.CreateSaturating``1(``0) @@ -1776,6 +1847,7 @@ System.Delegate.#ctor(System.Type,System.String) System.Delegate.Clone System.Delegate.Combine(System.Delegate,System.Delegate) System.Delegate.Combine(System.Delegate[]) +System.Delegate.Combine(System.ReadOnlySpan{System.Delegate}) System.Delegate.CombineImpl(System.Delegate) System.Delegate.CreateDelegate(System.Type,System.Object,System.Reflection.MethodInfo) System.Delegate.CreateDelegate(System.Type,System.Object,System.Reflection.MethodInfo,System.Boolean) @@ -1789,14 +1861,20 @@ System.Delegate.CreateDelegate(System.Type,System.Type,System.String,System.Bool System.Delegate.CreateDelegate(System.Type,System.Type,System.String,System.Boolean,System.Boolean) System.Delegate.DynamicInvoke(System.Object[]) System.Delegate.DynamicInvokeImpl(System.Object[]) +System.Delegate.EnumerateInvocationList``1(``0) System.Delegate.Equals(System.Object) System.Delegate.GetHashCode System.Delegate.GetInvocationList System.Delegate.GetMethodImpl System.Delegate.GetObjectData(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) +System.Delegate.InvocationListEnumerator`1 +System.Delegate.InvocationListEnumerator`1.GetEnumerator +System.Delegate.InvocationListEnumerator`1.MoveNext +System.Delegate.InvocationListEnumerator`1.get_Current System.Delegate.Remove(System.Delegate,System.Delegate) System.Delegate.RemoveAll(System.Delegate,System.Delegate) System.Delegate.RemoveImpl(System.Delegate) +System.Delegate.get_HasSingleTarget System.Delegate.get_Method System.Delegate.get_Target System.Delegate.op_Equality(System.Delegate,System.Delegate) @@ -1831,19 +1909,33 @@ System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute.get_TypeName System.Diagnostics.CodeAnalysis.DynamicDependencyAttribute.set_Condition(System.String) System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.AllConstructors +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.AllEvents +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.AllFields +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.AllMethods +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.AllNestedTypes +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.AllProperties System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructorsWithInherited System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicEvents +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicEventsWithInherited System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicFields +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicFieldsWithInherited System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethodsWithInherited System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypes +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicNestedTypesWithInherited System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicProperties +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicPropertiesWithInherited System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructorsWithInherited System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicEvents System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicFields System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypes +System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicNestedTypesWithInherited System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute @@ -1856,8 +1948,16 @@ System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute.set_Justificati System.Diagnostics.CodeAnalysis.ExperimentalAttribute System.Diagnostics.CodeAnalysis.ExperimentalAttribute.#ctor(System.String) System.Diagnostics.CodeAnalysis.ExperimentalAttribute.get_DiagnosticId +System.Diagnostics.CodeAnalysis.ExperimentalAttribute.get_Message System.Diagnostics.CodeAnalysis.ExperimentalAttribute.get_UrlFormat +System.Diagnostics.CodeAnalysis.ExperimentalAttribute.set_Message(System.String) System.Diagnostics.CodeAnalysis.ExperimentalAttribute.set_UrlFormat(System.String) +System.Diagnostics.CodeAnalysis.FeatureGuardAttribute +System.Diagnostics.CodeAnalysis.FeatureGuardAttribute.#ctor(System.Type) +System.Diagnostics.CodeAnalysis.FeatureGuardAttribute.get_FeatureType +System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute +System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute.#ctor(System.String) +System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute.get_SwitchName System.Diagnostics.CodeAnalysis.MaybeNullAttribute System.Diagnostics.CodeAnalysis.MaybeNullAttribute.#ctor System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute @@ -1888,13 +1988,17 @@ System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute.get_Url System.Diagnostics.CodeAnalysis.RequiresAssemblyFilesAttribute.set_Url(System.String) System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.#ctor(System.String) +System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.get_ExcludeStatics System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.get_Message System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.get_Url +System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.set_ExcludeStatics(System.Boolean) System.Diagnostics.CodeAnalysis.RequiresDynamicCodeAttribute.set_Url(System.String) System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.#ctor(System.String) +System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.get_ExcludeStatics System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.get_Message System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.get_Url +System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.set_ExcludeStatics(System.Boolean) System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute.set_Url(System.String) System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute.#ctor @@ -2005,6 +2109,8 @@ System.Diagnostics.DebuggerBrowsableState System.Diagnostics.DebuggerBrowsableState.Collapsed System.Diagnostics.DebuggerBrowsableState.Never System.Diagnostics.DebuggerBrowsableState.RootHidden +System.Diagnostics.DebuggerDisableUserUnhandledExceptionsAttribute +System.Diagnostics.DebuggerDisableUserUnhandledExceptionsAttribute.#ctor System.Diagnostics.DebuggerDisplayAttribute System.Diagnostics.DebuggerDisplayAttribute.#ctor(System.String) System.Diagnostics.DebuggerDisplayAttribute.get_Name @@ -2093,8 +2199,11 @@ System.Double.BitIncrement(System.Double) System.Double.Cbrt(System.Double) System.Double.Ceiling(System.Double) System.Double.Clamp(System.Double,System.Double,System.Double) +System.Double.ClampNative(System.Double,System.Double,System.Double) System.Double.CompareTo(System.Double) System.Double.CompareTo(System.Object) +System.Double.ConvertToIntegerNative``1(System.Double) +System.Double.ConvertToInteger``1(System.Double) System.Double.CopySign(System.Double,System.Double) System.Double.Cos(System.Double) System.Double.CosPi(System.Double) @@ -2145,13 +2254,16 @@ System.Double.LogP1(System.Double) System.Double.Max(System.Double,System.Double) System.Double.MaxMagnitude(System.Double,System.Double) System.Double.MaxMagnitudeNumber(System.Double,System.Double) +System.Double.MaxNative(System.Double,System.Double) System.Double.MaxNumber(System.Double,System.Double) System.Double.MaxValue System.Double.Min(System.Double,System.Double) System.Double.MinMagnitude(System.Double,System.Double) System.Double.MinMagnitudeNumber(System.Double,System.Double) +System.Double.MinNative(System.Double,System.Double) System.Double.MinNumber(System.Double,System.Double) System.Double.MinValue +System.Double.MultiplyAddEstimate(System.Double,System.Double,System.Double) System.Double.NaN System.Double.NegativeInfinity System.Double.NegativeZero @@ -2284,6 +2396,11 @@ System.EventHandler`1.#ctor(System.Object,System.IntPtr) System.EventHandler`1.BeginInvoke(System.Object,`0,System.AsyncCallback,System.Object) System.EventHandler`1.EndInvoke(System.IAsyncResult) System.EventHandler`1.Invoke(System.Object,`0) +System.EventHandler`2 +System.EventHandler`2.#ctor(System.Object,System.IntPtr) +System.EventHandler`2.BeginInvoke(`0,`1,System.AsyncCallback,System.Object) +System.EventHandler`2.EndInvoke(System.IAsyncResult) +System.EventHandler`2.Invoke(`0,`1) System.Exception System.Exception.#ctor System.Exception.#ctor(System.Runtime.Serialization.SerializationInfo,System.Runtime.Serialization.StreamingContext) @@ -2585,6 +2702,7 @@ System.Globalization.CompareOptions.IgnoreNonSpace System.Globalization.CompareOptions.IgnoreSymbols System.Globalization.CompareOptions.IgnoreWidth System.Globalization.CompareOptions.None +System.Globalization.CompareOptions.NumericOrdering System.Globalization.CompareOptions.Ordinal System.Globalization.CompareOptions.OrdinalIgnoreCase System.Globalization.CompareOptions.StringSort @@ -2861,11 +2979,14 @@ System.Globalization.HijriCalendar.get_TwoDigitYearMax System.Globalization.HijriCalendar.set_HijriAdjustment(System.Int32) System.Globalization.HijriCalendar.set_TwoDigitYearMax(System.Int32) System.Globalization.ISOWeek +System.Globalization.ISOWeek.GetWeekOfYear(System.DateOnly) System.Globalization.ISOWeek.GetWeekOfYear(System.DateTime) System.Globalization.ISOWeek.GetWeeksInYear(System.Int32) +System.Globalization.ISOWeek.GetYear(System.DateOnly) System.Globalization.ISOWeek.GetYear(System.DateTime) System.Globalization.ISOWeek.GetYearEnd(System.Int32) System.Globalization.ISOWeek.GetYearStart(System.Int32) +System.Globalization.ISOWeek.ToDateOnly(System.Int32,System.Int32,System.DayOfWeek) System.Globalization.ISOWeek.ToDateTime(System.Int32,System.Int32,System.DayOfWeek) System.Globalization.IdnMapping System.Globalization.IdnMapping.#ctor @@ -3299,11 +3420,15 @@ System.Guid.#ctor(System.String) System.Guid.#ctor(System.UInt32,System.UInt16,System.UInt16,System.Byte,System.Byte,System.Byte,System.Byte,System.Byte,System.Byte,System.Byte,System.Byte) System.Guid.CompareTo(System.Guid) System.Guid.CompareTo(System.Object) +System.Guid.CreateVersion7 +System.Guid.CreateVersion7(System.DateTimeOffset) System.Guid.Empty System.Guid.Equals(System.Guid) System.Guid.Equals(System.Object) System.Guid.GetHashCode System.Guid.NewGuid +System.Guid.Parse(System.ReadOnlySpan{System.Byte}) +System.Guid.Parse(System.ReadOnlySpan{System.Byte},System.IFormatProvider) System.Guid.Parse(System.ReadOnlySpan{System.Char}) System.Guid.Parse(System.ReadOnlySpan{System.Char},System.IFormatProvider) System.Guid.Parse(System.String) @@ -3317,6 +3442,8 @@ System.Guid.ToString(System.String) System.Guid.ToString(System.String,System.IFormatProvider) System.Guid.TryFormat(System.Span{System.Byte},System.Int32@,System.ReadOnlySpan{System.Char}) System.Guid.TryFormat(System.Span{System.Char},System.Int32@,System.ReadOnlySpan{System.Char}) +System.Guid.TryParse(System.ReadOnlySpan{System.Byte},System.Guid@) +System.Guid.TryParse(System.ReadOnlySpan{System.Byte},System.IFormatProvider,System.Guid@) System.Guid.TryParse(System.ReadOnlySpan{System.Char},System.Guid@) System.Guid.TryParse(System.ReadOnlySpan{System.Char},System.IFormatProvider,System.Guid@) System.Guid.TryParse(System.String,System.Guid@) @@ -3325,6 +3452,9 @@ System.Guid.TryParseExact(System.ReadOnlySpan{System.Char},System.ReadOnlySpan{S System.Guid.TryParseExact(System.String,System.String,System.Guid@) System.Guid.TryWriteBytes(System.Span{System.Byte}) System.Guid.TryWriteBytes(System.Span{System.Byte},System.Boolean,System.Int32@) +System.Guid.get_AllBitsSet +System.Guid.get_Variant +System.Guid.get_Version System.Guid.op_Equality(System.Guid,System.Guid) System.Guid.op_GreaterThan(System.Guid,System.Guid) System.Guid.op_GreaterThanOrEqual(System.Guid,System.Guid) @@ -3349,8 +3479,11 @@ System.Half.BitIncrement(System.Half) System.Half.Cbrt(System.Half) System.Half.Ceiling(System.Half) System.Half.Clamp(System.Half,System.Half,System.Half) +System.Half.ClampNative(System.Half,System.Half,System.Half) System.Half.CompareTo(System.Half) System.Half.CompareTo(System.Object) +System.Half.ConvertToIntegerNative``1(System.Half) +System.Half.ConvertToInteger``1(System.Half) System.Half.CopySign(System.Half,System.Half) System.Half.Cos(System.Half) System.Half.CosPi(System.Half) @@ -3398,11 +3531,14 @@ System.Half.LogP1(System.Half) System.Half.Max(System.Half,System.Half) System.Half.MaxMagnitude(System.Half,System.Half) System.Half.MaxMagnitudeNumber(System.Half,System.Half) +System.Half.MaxNative(System.Half,System.Half) System.Half.MaxNumber(System.Half,System.Half) System.Half.Min(System.Half,System.Half) System.Half.MinMagnitude(System.Half,System.Half) System.Half.MinMagnitudeNumber(System.Half,System.Half) +System.Half.MinNative(System.Half,System.Half) System.Half.MinNumber(System.Half,System.Half) +System.Half.MultiplyAddEstimate(System.Half,System.Half,System.Half) System.Half.Parse(System.ReadOnlySpan{System.Byte},System.Globalization.NumberStyles,System.IFormatProvider) System.Half.Parse(System.ReadOnlySpan{System.Byte},System.IFormatProvider) System.Half.Parse(System.ReadOnlySpan{System.Char},System.Globalization.NumberStyles,System.IFormatProvider) @@ -3627,6 +3763,7 @@ System.InsufficientMemoryException.#ctor(System.String,System.Exception) System.Int128 System.Int128.#ctor(System.UInt64,System.UInt64) System.Int128.Abs(System.Int128) +System.Int128.BigMul(System.Int128,System.Int128,System.Int128@) System.Int128.Clamp(System.Int128,System.Int128,System.Int128) System.Int128.CompareTo(System.Int128) System.Int128.CompareTo(System.Object) @@ -3812,6 +3949,7 @@ System.Int16.TryParse(System.String,System.IFormatProvider,System.Int16@) System.Int16.TryParse(System.String,System.Int16@) System.Int32 System.Int32.Abs(System.Int32) +System.Int32.BigMul(System.Int32,System.Int32) System.Int32.Clamp(System.Int32,System.Int32,System.Int32) System.Int32.CompareTo(System.Int32) System.Int32.CompareTo(System.Object) @@ -3867,6 +4005,7 @@ System.Int32.TryParse(System.String,System.IFormatProvider,System.Int32@) System.Int32.TryParse(System.String,System.Int32@) System.Int64 System.Int64.Abs(System.Int64) +System.Int64.BigMul(System.Int64,System.Int64) System.Int64.Clamp(System.Int64,System.Int64,System.Int64) System.Int64.CompareTo(System.Int64) System.Int64.CompareTo(System.Object) @@ -3926,6 +4065,7 @@ System.IntPtr.#ctor(System.Int64) System.IntPtr.#ctor(System.Void*) System.IntPtr.Abs(System.IntPtr) System.IntPtr.Add(System.IntPtr,System.Int32) +System.IntPtr.BigMul(System.IntPtr,System.IntPtr,System.IntPtr@) System.IntPtr.Clamp(System.IntPtr,System.IntPtr,System.IntPtr) System.IntPtr.CompareTo(System.IntPtr) System.IntPtr.CompareTo(System.Object) @@ -4052,7 +4192,10 @@ System.Math.Atan(System.Double) System.Math.Atan2(System.Double,System.Double) System.Math.Atanh(System.Double) System.Math.BigMul(System.Int32,System.Int32) +System.Math.BigMul(System.Int64,System.Int64) System.Math.BigMul(System.Int64,System.Int64,System.Int64@) +System.Math.BigMul(System.UInt32,System.UInt32) +System.Math.BigMul(System.UInt64,System.UInt64) System.Math.BigMul(System.UInt64,System.UInt64,System.UInt64@) System.Math.BitDecrement(System.Double) System.Math.BitIncrement(System.Double) @@ -4449,6 +4592,8 @@ System.Numerics.IFloatingPointIeee754`1.get_NegativeZero System.Numerics.IFloatingPointIeee754`1.get_PositiveInfinity System.Numerics.IFloatingPoint`1 System.Numerics.IFloatingPoint`1.Ceiling(`0) +System.Numerics.IFloatingPoint`1.ConvertToIntegerNative``1(`0) +System.Numerics.IFloatingPoint`1.ConvertToInteger``1(`0) System.Numerics.IFloatingPoint`1.Floor(`0) System.Numerics.IFloatingPoint`1.GetExponentByteCount System.Numerics.IFloatingPoint`1.GetExponentShortestBitLength @@ -4529,6 +4674,7 @@ System.Numerics.INumberBase`1.MaxMagnitude(`0,`0) System.Numerics.INumberBase`1.MaxMagnitudeNumber(`0,`0) System.Numerics.INumberBase`1.MinMagnitude(`0,`0) System.Numerics.INumberBase`1.MinMagnitudeNumber(`0,`0) +System.Numerics.INumberBase`1.MultiplyAddEstimate(`0,`0,`0) System.Numerics.INumberBase`1.Parse(System.ReadOnlySpan{System.Byte},System.Globalization.NumberStyles,System.IFormatProvider) System.Numerics.INumberBase`1.Parse(System.ReadOnlySpan{System.Char},System.Globalization.NumberStyles,System.IFormatProvider) System.Numerics.INumberBase`1.Parse(System.String,System.Globalization.NumberStyles,System.IFormatProvider) @@ -4546,10 +4692,13 @@ System.Numerics.INumberBase`1.get_Radix System.Numerics.INumberBase`1.get_Zero System.Numerics.INumber`1 System.Numerics.INumber`1.Clamp(`0,`0,`0) +System.Numerics.INumber`1.ClampNative(`0,`0,`0) System.Numerics.INumber`1.CopySign(`0,`0) System.Numerics.INumber`1.Max(`0,`0) +System.Numerics.INumber`1.MaxNative(`0,`0) System.Numerics.INumber`1.MaxNumber(`0,`0) System.Numerics.INumber`1.Min(`0,`0) +System.Numerics.INumber`1.MinNative(`0,`0) System.Numerics.INumber`1.MinNumber(`0,`0) System.Numerics.INumber`1.Sign(`0) System.Numerics.IPowerFunctions`1 @@ -4707,9 +4856,12 @@ System.Progress`1.remove_ProgressChanged(System.EventHandler{`0}) System.Random System.Random.#ctor System.Random.#ctor(System.Int32) +System.Random.GetHexString(System.Int32,System.Boolean) +System.Random.GetHexString(System.Span{System.Char},System.Boolean) System.Random.GetItems``1(System.ReadOnlySpan{``0},System.Int32) System.Random.GetItems``1(System.ReadOnlySpan{``0},System.Span{``0}) System.Random.GetItems``1(``0[],System.Int32) +System.Random.GetString(System.ReadOnlySpan{System.Char},System.Int32) System.Random.Next System.Random.Next(System.Int32) System.Random.Next(System.Int32,System.Int32) @@ -4765,6 +4917,7 @@ System.ReadOnlySpan`1.#ctor(System.Void*,System.Int32) System.ReadOnlySpan`1.#ctor(`0@) System.ReadOnlySpan`1.#ctor(`0[]) System.ReadOnlySpan`1.#ctor(`0[],System.Int32,System.Int32) +System.ReadOnlySpan`1.CastUp``1(System.ReadOnlySpan{``0}) System.ReadOnlySpan`1.CopyTo(System.Span{`0}) System.ReadOnlySpan`1.Enumerator System.ReadOnlySpan`1.Enumerator.MoveNext @@ -4799,6 +4952,17 @@ System.ResolveEventHandler.Invoke(System.Object,System.ResolveEventArgs) System.Runtime.CompilerServices.AccessedThroughPropertyAttribute System.Runtime.CompilerServices.AccessedThroughPropertyAttribute.#ctor(System.String) System.Runtime.CompilerServices.AccessedThroughPropertyAttribute.get_PropertyName +System.Runtime.CompilerServices.AsyncHelpers +System.Runtime.CompilerServices.AsyncHelpers.Await(System.Runtime.CompilerServices.ConfiguredTaskAwaitable) +System.Runtime.CompilerServices.AsyncHelpers.Await(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable) +System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.Task) +System.Runtime.CompilerServices.AsyncHelpers.Await(System.Threading.Tasks.ValueTask) +System.Runtime.CompilerServices.AsyncHelpers.AwaitAwaiter``1(``0) +System.Runtime.CompilerServices.AsyncHelpers.Await``1(System.Runtime.CompilerServices.ConfiguredTaskAwaitable{``0}) +System.Runtime.CompilerServices.AsyncHelpers.Await``1(System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable{``0}) +System.Runtime.CompilerServices.AsyncHelpers.Await``1(System.Threading.Tasks.Task{``0}) +System.Runtime.CompilerServices.AsyncHelpers.Await``1(System.Threading.Tasks.ValueTask{``0}) +System.Runtime.CompilerServices.AsyncHelpers.UnsafeAwaitAwaiter``1(``0) System.Runtime.CompilerServices.AsyncIteratorMethodBuilder System.Runtime.CompilerServices.AsyncIteratorMethodBuilder.AwaitOnCompleted``2(``0@,``1@) System.Runtime.CompilerServices.AsyncIteratorMethodBuilder.AwaitUnsafeOnCompleted``2(``0@,``1@) @@ -4866,6 +5030,8 @@ System.Runtime.CompilerServices.CallConvStdcall System.Runtime.CompilerServices.CallConvStdcall.#ctor System.Runtime.CompilerServices.CallConvSuppressGCTransition System.Runtime.CompilerServices.CallConvSuppressGCTransition.#ctor +System.Runtime.CompilerServices.CallConvSwift +System.Runtime.CompilerServices.CallConvSwift.#ctor System.Runtime.CompilerServices.CallConvThiscall System.Runtime.CompilerServices.CallConvThiscall.#ctor System.Runtime.CompilerServices.CallerArgumentExpressionAttribute @@ -4898,6 +5064,8 @@ System.Runtime.CompilerServices.CompilerGeneratedAttribute System.Runtime.CompilerServices.CompilerGeneratedAttribute.#ctor System.Runtime.CompilerServices.CompilerGlobalScopeAttribute System.Runtime.CompilerServices.CompilerGlobalScopeAttribute.#ctor +System.Runtime.CompilerServices.CompilerLoweringPreserveAttribute +System.Runtime.CompilerServices.CompilerLoweringPreserveAttribute.#ctor System.Runtime.CompilerServices.ConditionalWeakTable`2 System.Runtime.CompilerServices.ConditionalWeakTable`2.#ctor System.Runtime.CompilerServices.ConditionalWeakTable`2.Add(`0,`1) @@ -4908,9 +5076,13 @@ System.Runtime.CompilerServices.ConditionalWeakTable`2.CreateValueCallback.#ctor System.Runtime.CompilerServices.ConditionalWeakTable`2.CreateValueCallback.BeginInvoke(`0,System.AsyncCallback,System.Object) System.Runtime.CompilerServices.ConditionalWeakTable`2.CreateValueCallback.EndInvoke(System.IAsyncResult) System.Runtime.CompilerServices.ConditionalWeakTable`2.CreateValueCallback.Invoke(`0) +System.Runtime.CompilerServices.ConditionalWeakTable`2.GetOrAdd(`0,System.Func{`0,`1}) +System.Runtime.CompilerServices.ConditionalWeakTable`2.GetOrAdd(`0,`1) +System.Runtime.CompilerServices.ConditionalWeakTable`2.GetOrAdd``1(`0,System.Func{`0,``0,`1},``0) System.Runtime.CompilerServices.ConditionalWeakTable`2.GetOrCreateValue(`0) System.Runtime.CompilerServices.ConditionalWeakTable`2.GetValue(`0,System.Runtime.CompilerServices.ConditionalWeakTable{`0,`1}.CreateValueCallback) System.Runtime.CompilerServices.ConditionalWeakTable`2.Remove(`0) +System.Runtime.CompilerServices.ConditionalWeakTable`2.Remove(`0,`1@) System.Runtime.CompilerServices.ConditionalWeakTable`2.TryAdd(`0,`1) System.Runtime.CompilerServices.ConditionalWeakTable`2.TryGetValue(`0,`1@) System.Runtime.CompilerServices.ConfiguredAsyncDisposable @@ -4978,8 +5150,10 @@ System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted``1(``0,System.Int32,System.String) System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted``1(``0,System.String) System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendLiteral(System.String) +System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.Clear System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToString System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear +System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.get_Text System.Runtime.CompilerServices.DependencyAttribute System.Runtime.CompilerServices.DependencyAttribute.#ctor(System.String,System.Runtime.CompilerServices.LoadHint) System.Runtime.CompilerServices.DependencyAttribute.get_DependentAssembly @@ -4994,6 +5168,9 @@ System.Runtime.CompilerServices.EnumeratorCancellationAttribute System.Runtime.CompilerServices.EnumeratorCancellationAttribute.#ctor System.Runtime.CompilerServices.ExtensionAttribute System.Runtime.CompilerServices.ExtensionAttribute.#ctor +System.Runtime.CompilerServices.ExtensionMarkerAttribute +System.Runtime.CompilerServices.ExtensionMarkerAttribute.#ctor(System.String) +System.Runtime.CompilerServices.ExtensionMarkerAttribute.get_Name System.Runtime.CompilerServices.FixedAddressValueTypeAttribute System.Runtime.CompilerServices.FixedAddressValueTypeAttribute.#ctor System.Runtime.CompilerServices.FixedBufferAttribute @@ -5017,6 +5194,21 @@ System.Runtime.CompilerServices.ITuple.get_Item(System.Int32) System.Runtime.CompilerServices.ITuple.get_Length System.Runtime.CompilerServices.IndexerNameAttribute System.Runtime.CompilerServices.IndexerNameAttribute.#ctor(System.String) +System.Runtime.CompilerServices.InlineArray10`1 +System.Runtime.CompilerServices.InlineArray11`1 +System.Runtime.CompilerServices.InlineArray12`1 +System.Runtime.CompilerServices.InlineArray13`1 +System.Runtime.CompilerServices.InlineArray14`1 +System.Runtime.CompilerServices.InlineArray15`1 +System.Runtime.CompilerServices.InlineArray16`1 +System.Runtime.CompilerServices.InlineArray2`1 +System.Runtime.CompilerServices.InlineArray3`1 +System.Runtime.CompilerServices.InlineArray4`1 +System.Runtime.CompilerServices.InlineArray5`1 +System.Runtime.CompilerServices.InlineArray6`1 +System.Runtime.CompilerServices.InlineArray7`1 +System.Runtime.CompilerServices.InlineArray8`1 +System.Runtime.CompilerServices.InlineArray9`1 System.Runtime.CompilerServices.InlineArrayAttribute System.Runtime.CompilerServices.InlineArrayAttribute.#ctor(System.Int32) System.Runtime.CompilerServices.InlineArrayAttribute.get_Length @@ -5069,6 +5261,11 @@ System.Runtime.CompilerServices.NullableContextAttribute.Flag System.Runtime.CompilerServices.NullablePublicOnlyAttribute System.Runtime.CompilerServices.NullablePublicOnlyAttribute.#ctor(System.Boolean) System.Runtime.CompilerServices.NullablePublicOnlyAttribute.IncludesInternals +System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute +System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute.#ctor(System.Int32) +System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute.get_Priority +System.Runtime.CompilerServices.ParamCollectionAttribute +System.Runtime.CompilerServices.ParamCollectionAttribute.#ctor System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder.AwaitOnCompleted``2(``0@,``1@) System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder.AwaitUnsafeOnCompleted``2(``0@,``1@) @@ -5106,6 +5303,7 @@ System.Runtime.CompilerServices.RuntimeCompatibilityAttribute.get_WrapNonExcepti System.Runtime.CompilerServices.RuntimeCompatibilityAttribute.set_WrapNonExceptionThrows(System.Boolean) System.Runtime.CompilerServices.RuntimeFeature System.Runtime.CompilerServices.RuntimeFeature.ByRefFields +System.Runtime.CompilerServices.RuntimeFeature.ByRefLikeGenerics System.Runtime.CompilerServices.RuntimeFeature.CovariantReturnsOfClasses System.Runtime.CompilerServices.RuntimeFeature.DefaultImplementationsOfInterfaces System.Runtime.CompilerServices.RuntimeFeature.IsSupported(System.String) @@ -5117,6 +5315,7 @@ System.Runtime.CompilerServices.RuntimeFeature.get_IsDynamicCodeCompiled System.Runtime.CompilerServices.RuntimeFeature.get_IsDynamicCodeSupported System.Runtime.CompilerServices.RuntimeHelpers System.Runtime.CompilerServices.RuntimeHelpers.AllocateTypeAssociatedMemory(System.Type,System.Int32) +System.Runtime.CompilerServices.RuntimeHelpers.Box(System.Byte@,System.RuntimeTypeHandle) System.Runtime.CompilerServices.RuntimeHelpers.CleanupCode System.Runtime.CompilerServices.RuntimeHelpers.CleanupCode.#ctor(System.Object,System.IntPtr) System.Runtime.CompilerServices.RuntimeHelpers.CleanupCode.BeginInvoke(System.Object,System.Boolean,System.AsyncCallback,System.Object) @@ -5141,6 +5340,7 @@ System.Runtime.CompilerServices.RuntimeHelpers.PrepareMethod(System.RuntimeMetho System.Runtime.CompilerServices.RuntimeHelpers.ProbeForSufficientStack System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(System.RuntimeTypeHandle) System.Runtime.CompilerServices.RuntimeHelpers.RunModuleConstructor(System.ModuleHandle) +System.Runtime.CompilerServices.RuntimeHelpers.SizeOf(System.RuntimeTypeHandle) System.Runtime.CompilerServices.RuntimeHelpers.TryCode System.Runtime.CompilerServices.RuntimeHelpers.TryCode.#ctor(System.Object,System.IntPtr) System.Runtime.CompilerServices.RuntimeHelpers.TryCode.BeginInvoke(System.Object,System.AsyncCallback,System.Object) @@ -5222,7 +5422,9 @@ System.Runtime.CompilerServices.Unsafe.InitBlock(System.Byte@,System.Byte,System System.Runtime.CompilerServices.Unsafe.InitBlock(System.Void*,System.Byte,System.UInt32) System.Runtime.CompilerServices.Unsafe.InitBlockUnaligned(System.Byte@,System.Byte,System.UInt32) System.Runtime.CompilerServices.Unsafe.InitBlockUnaligned(System.Void*,System.Byte,System.UInt32) +System.Runtime.CompilerServices.Unsafe.IsAddressGreaterThanOrEqualTo``1(``0@,``0@) System.Runtime.CompilerServices.Unsafe.IsAddressGreaterThan``1(``0@,``0@) +System.Runtime.CompilerServices.Unsafe.IsAddressLessThanOrEqualTo``1(``0@,``0@) System.Runtime.CompilerServices.Unsafe.IsAddressLessThan``1(``0@,``0@) System.Runtime.CompilerServices.Unsafe.IsNullRef``1(``0@) System.Runtime.CompilerServices.Unsafe.NullRef``1 @@ -5252,6 +5454,9 @@ System.Runtime.CompilerServices.UnsafeAccessorKind.Field System.Runtime.CompilerServices.UnsafeAccessorKind.Method System.Runtime.CompilerServices.UnsafeAccessorKind.StaticField System.Runtime.CompilerServices.UnsafeAccessorKind.StaticMethod +System.Runtime.CompilerServices.UnsafeAccessorTypeAttribute +System.Runtime.CompilerServices.UnsafeAccessorTypeAttribute.#ctor(System.String) +System.Runtime.CompilerServices.UnsafeAccessorTypeAttribute.get_TypeName System.Runtime.CompilerServices.UnsafeValueTypeAttribute System.Runtime.CompilerServices.UnsafeValueTypeAttribute.#ctor System.Runtime.CompilerServices.ValueTaskAwaiter @@ -5381,8 +5586,11 @@ System.Single.BitIncrement(System.Single) System.Single.Cbrt(System.Single) System.Single.Ceiling(System.Single) System.Single.Clamp(System.Single,System.Single,System.Single) +System.Single.ClampNative(System.Single,System.Single,System.Single) System.Single.CompareTo(System.Object) System.Single.CompareTo(System.Single) +System.Single.ConvertToIntegerNative``1(System.Single) +System.Single.ConvertToInteger``1(System.Single) System.Single.CopySign(System.Single,System.Single) System.Single.Cos(System.Single) System.Single.CosPi(System.Single) @@ -5433,13 +5641,16 @@ System.Single.LogP1(System.Single) System.Single.Max(System.Single,System.Single) System.Single.MaxMagnitude(System.Single,System.Single) System.Single.MaxMagnitudeNumber(System.Single,System.Single) +System.Single.MaxNative(System.Single,System.Single) System.Single.MaxNumber(System.Single,System.Single) System.Single.MaxValue System.Single.Min(System.Single,System.Single) System.Single.MinMagnitude(System.Single,System.Single) System.Single.MinMagnitudeNumber(System.Single,System.Single) +System.Single.MinNative(System.Single,System.Single) System.Single.MinNumber(System.Single,System.Single) System.Single.MinValue +System.Single.MultiplyAddEstimate(System.Single,System.Single,System.Single) System.Single.NaN System.Single.NegativeInfinity System.Single.NegativeZero @@ -5562,6 +5773,8 @@ System.String.Concat(System.Object[]) System.String.Concat(System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char}) System.String.Concat(System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char}) System.String.Concat(System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char}) +System.String.Concat(System.ReadOnlySpan{System.Object}) +System.String.Concat(System.ReadOnlySpan{System.String}) System.String.Concat(System.String,System.String) System.String.Concat(System.String,System.String,System.String) System.String.Concat(System.String,System.String,System.String,System.String) @@ -5592,12 +5805,14 @@ System.String.Format(System.IFormatProvider,System.String,System.Object) System.String.Format(System.IFormatProvider,System.String,System.Object,System.Object) System.String.Format(System.IFormatProvider,System.String,System.Object,System.Object,System.Object) System.String.Format(System.IFormatProvider,System.String,System.Object[]) +System.String.Format(System.IFormatProvider,System.String,System.ReadOnlySpan{System.Object}) System.String.Format(System.IFormatProvider,System.Text.CompositeFormat,System.Object[]) System.String.Format(System.IFormatProvider,System.Text.CompositeFormat,System.ReadOnlySpan{System.Object}) System.String.Format(System.String,System.Object) System.String.Format(System.String,System.Object,System.Object) System.String.Format(System.String,System.Object,System.Object,System.Object) System.String.Format(System.String,System.Object[]) +System.String.Format(System.String,System.ReadOnlySpan{System.Object}) System.String.Format``1(System.IFormatProvider,System.Text.CompositeFormat,``0) System.String.Format``2(System.IFormatProvider,System.Text.CompositeFormat,``0,``1) System.String.Format``3(System.IFormatProvider,System.Text.CompositeFormat,``0,``1,``2) @@ -5629,10 +5844,14 @@ System.String.IsNormalized(System.Text.NormalizationForm) System.String.IsNullOrEmpty(System.String) System.String.IsNullOrWhiteSpace(System.String) System.String.Join(System.Char,System.Object[]) +System.String.Join(System.Char,System.ReadOnlySpan{System.Object}) +System.String.Join(System.Char,System.ReadOnlySpan{System.String}) System.String.Join(System.Char,System.String[]) System.String.Join(System.Char,System.String[],System.Int32,System.Int32) System.String.Join(System.String,System.Collections.Generic.IEnumerable{System.String}) System.String.Join(System.String,System.Object[]) +System.String.Join(System.String,System.ReadOnlySpan{System.Object}) +System.String.Join(System.String,System.ReadOnlySpan{System.String}) System.String.Join(System.String,System.String[]) System.String.Join(System.String,System.String[],System.Int32,System.Int32) System.String.Join``1(System.Char,System.Collections.Generic.IEnumerable{``0}) @@ -5669,6 +5888,7 @@ System.String.Split(System.Char[]) System.String.Split(System.Char[],System.Int32) System.String.Split(System.Char[],System.Int32,System.StringSplitOptions) System.String.Split(System.Char[],System.StringSplitOptions) +System.String.Split(System.ReadOnlySpan{System.Char}) System.String.Split(System.String,System.Int32,System.StringSplitOptions) System.String.Split(System.String,System.StringSplitOptions) System.String.Split(System.String[],System.Int32,System.StringSplitOptions) @@ -5731,10 +5951,13 @@ System.StringComparison.InvariantCultureIgnoreCase System.StringComparison.Ordinal System.StringComparison.OrdinalIgnoreCase System.StringNormalizationExtensions +System.StringNormalizationExtensions.GetNormalizedLength(System.ReadOnlySpan{System.Char},System.Text.NormalizationForm) +System.StringNormalizationExtensions.IsNormalized(System.ReadOnlySpan{System.Char},System.Text.NormalizationForm) System.StringNormalizationExtensions.IsNormalized(System.String) System.StringNormalizationExtensions.IsNormalized(System.String,System.Text.NormalizationForm) System.StringNormalizationExtensions.Normalize(System.String) System.StringNormalizationExtensions.Normalize(System.String,System.Text.NormalizationForm) +System.StringNormalizationExtensions.TryNormalize(System.ReadOnlySpan{System.Char},System.Span{System.Char},System.Int32@,System.Text.NormalizationForm) System.StringSplitOptions System.StringSplitOptions.None System.StringSplitOptions.RemoveEmptyEntries @@ -6105,12 +6328,14 @@ System.Text.StringBuilder.AppendFormat(System.IFormatProvider,System.String,Syst System.Text.StringBuilder.AppendFormat(System.IFormatProvider,System.String,System.Object,System.Object) System.Text.StringBuilder.AppendFormat(System.IFormatProvider,System.String,System.Object,System.Object,System.Object) System.Text.StringBuilder.AppendFormat(System.IFormatProvider,System.String,System.Object[]) +System.Text.StringBuilder.AppendFormat(System.IFormatProvider,System.String,System.ReadOnlySpan{System.Object}) System.Text.StringBuilder.AppendFormat(System.IFormatProvider,System.Text.CompositeFormat,System.Object[]) System.Text.StringBuilder.AppendFormat(System.IFormatProvider,System.Text.CompositeFormat,System.ReadOnlySpan{System.Object}) System.Text.StringBuilder.AppendFormat(System.String,System.Object) System.Text.StringBuilder.AppendFormat(System.String,System.Object,System.Object) System.Text.StringBuilder.AppendFormat(System.String,System.Object,System.Object,System.Object) System.Text.StringBuilder.AppendFormat(System.String,System.Object[]) +System.Text.StringBuilder.AppendFormat(System.String,System.ReadOnlySpan{System.Object}) System.Text.StringBuilder.AppendFormat``1(System.IFormatProvider,System.Text.CompositeFormat,``0) System.Text.StringBuilder.AppendFormat``2(System.IFormatProvider,System.Text.CompositeFormat,``0,``1) System.Text.StringBuilder.AppendFormat``3(System.IFormatProvider,System.Text.CompositeFormat,``0,``1,``2) @@ -6128,8 +6353,12 @@ System.Text.StringBuilder.AppendInterpolatedStringHandler.AppendFormatted``1(``0 System.Text.StringBuilder.AppendInterpolatedStringHandler.AppendFormatted``1(``0,System.String) System.Text.StringBuilder.AppendInterpolatedStringHandler.AppendLiteral(System.String) System.Text.StringBuilder.AppendJoin(System.Char,System.Object[]) +System.Text.StringBuilder.AppendJoin(System.Char,System.ReadOnlySpan{System.Object}) +System.Text.StringBuilder.AppendJoin(System.Char,System.ReadOnlySpan{System.String}) System.Text.StringBuilder.AppendJoin(System.Char,System.String[]) System.Text.StringBuilder.AppendJoin(System.String,System.Object[]) +System.Text.StringBuilder.AppendJoin(System.String,System.ReadOnlySpan{System.Object}) +System.Text.StringBuilder.AppendJoin(System.String,System.ReadOnlySpan{System.String}) System.Text.StringBuilder.AppendJoin(System.String,System.String[]) System.Text.StringBuilder.AppendJoin``1(System.Char,System.Collections.Generic.IEnumerable{``0}) System.Text.StringBuilder.AppendJoin``1(System.String,System.Collections.Generic.IEnumerable{``0}) @@ -6170,6 +6399,8 @@ System.Text.StringBuilder.Insert(System.Int32,System.UInt64) System.Text.StringBuilder.Remove(System.Int32,System.Int32) System.Text.StringBuilder.Replace(System.Char,System.Char) System.Text.StringBuilder.Replace(System.Char,System.Char,System.Int32,System.Int32) +System.Text.StringBuilder.Replace(System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char}) +System.Text.StringBuilder.Replace(System.ReadOnlySpan{System.Char},System.ReadOnlySpan{System.Char},System.Int32,System.Int32) System.Text.StringBuilder.Replace(System.String,System.String) System.Text.StringBuilder.Replace(System.String,System.String,System.Int32,System.Int32) System.Text.StringBuilder.ToString @@ -6235,6 +6466,7 @@ System.Threading.CancellationTokenSource.Cancel(System.Boolean) System.Threading.CancellationTokenSource.CancelAfter(System.Int32) System.Threading.CancellationTokenSource.CancelAfter(System.TimeSpan) System.Threading.CancellationTokenSource.CancelAsync +System.Threading.CancellationTokenSource.CreateLinkedTokenSource(System.ReadOnlySpan{System.Threading.CancellationToken}) System.Threading.CancellationTokenSource.CreateLinkedTokenSource(System.Threading.CancellationToken) System.Threading.CancellationTokenSource.CreateLinkedTokenSource(System.Threading.CancellationToken,System.Threading.CancellationToken) System.Threading.CancellationTokenSource.CreateLinkedTokenSource(System.Threading.CancellationToken[]) @@ -6329,11 +6561,13 @@ System.Threading.Tasks.TaskCompletionSource.SetCanceled System.Threading.Tasks.TaskCompletionSource.SetCanceled(System.Threading.CancellationToken) System.Threading.Tasks.TaskCompletionSource.SetException(System.Collections.Generic.IEnumerable{System.Exception}) System.Threading.Tasks.TaskCompletionSource.SetException(System.Exception) +System.Threading.Tasks.TaskCompletionSource.SetFromTask(System.Threading.Tasks.Task) System.Threading.Tasks.TaskCompletionSource.SetResult System.Threading.Tasks.TaskCompletionSource.TrySetCanceled System.Threading.Tasks.TaskCompletionSource.TrySetCanceled(System.Threading.CancellationToken) System.Threading.Tasks.TaskCompletionSource.TrySetException(System.Collections.Generic.IEnumerable{System.Exception}) System.Threading.Tasks.TaskCompletionSource.TrySetException(System.Exception) +System.Threading.Tasks.TaskCompletionSource.TrySetFromTask(System.Threading.Tasks.Task) System.Threading.Tasks.TaskCompletionSource.TrySetResult System.Threading.Tasks.TaskCompletionSource.get_Task System.Threading.Tasks.TaskCompletionSource`1 @@ -6345,11 +6579,13 @@ System.Threading.Tasks.TaskCompletionSource`1.SetCanceled System.Threading.Tasks.TaskCompletionSource`1.SetCanceled(System.Threading.CancellationToken) System.Threading.Tasks.TaskCompletionSource`1.SetException(System.Collections.Generic.IEnumerable{System.Exception}) System.Threading.Tasks.TaskCompletionSource`1.SetException(System.Exception) +System.Threading.Tasks.TaskCompletionSource`1.SetFromTask(System.Threading.Tasks.Task{`0}) System.Threading.Tasks.TaskCompletionSource`1.SetResult(`0) System.Threading.Tasks.TaskCompletionSource`1.TrySetCanceled System.Threading.Tasks.TaskCompletionSource`1.TrySetCanceled(System.Threading.CancellationToken) System.Threading.Tasks.TaskCompletionSource`1.TrySetException(System.Collections.Generic.IEnumerable{System.Exception}) System.Threading.Tasks.TaskCompletionSource`1.TrySetException(System.Exception) +System.Threading.Tasks.TaskCompletionSource`1.TrySetFromTask(System.Threading.Tasks.Task{`0}) System.Threading.Tasks.TaskCompletionSource`1.TrySetResult(`0) System.Threading.Tasks.TaskCompletionSource`1.get_Task System.Threading.Tasks.TaskContinuationOptions @@ -6555,15 +6791,38 @@ System.TimeSpan.Equals(System.Object) System.TimeSpan.Equals(System.TimeSpan) System.TimeSpan.Equals(System.TimeSpan,System.TimeSpan) System.TimeSpan.FromDays(System.Double) +System.TimeSpan.FromDays(System.Int32) +System.TimeSpan.FromDays(System.Int32,System.Int32,System.Int64,System.Int64,System.Int64,System.Int64) System.TimeSpan.FromHours(System.Double) +System.TimeSpan.FromHours(System.Int32) +System.TimeSpan.FromHours(System.Int32,System.Int64,System.Int64,System.Int64,System.Int64) System.TimeSpan.FromMicroseconds(System.Double) +System.TimeSpan.FromMicroseconds(System.Int64) System.TimeSpan.FromMilliseconds(System.Double) +System.TimeSpan.FromMilliseconds(System.Int64) +System.TimeSpan.FromMilliseconds(System.Int64,System.Int64) System.TimeSpan.FromMinutes(System.Double) +System.TimeSpan.FromMinutes(System.Int64) +System.TimeSpan.FromMinutes(System.Int64,System.Int64,System.Int64,System.Int64) System.TimeSpan.FromSeconds(System.Double) +System.TimeSpan.FromSeconds(System.Int64) +System.TimeSpan.FromSeconds(System.Int64,System.Int64,System.Int64) System.TimeSpan.FromTicks(System.Int64) System.TimeSpan.GetHashCode +System.TimeSpan.HoursPerDay System.TimeSpan.MaxValue +System.TimeSpan.MicrosecondsPerDay +System.TimeSpan.MicrosecondsPerHour +System.TimeSpan.MicrosecondsPerMillisecond +System.TimeSpan.MicrosecondsPerMinute +System.TimeSpan.MicrosecondsPerSecond +System.TimeSpan.MillisecondsPerDay +System.TimeSpan.MillisecondsPerHour +System.TimeSpan.MillisecondsPerMinute +System.TimeSpan.MillisecondsPerSecond System.TimeSpan.MinValue +System.TimeSpan.MinutesPerDay +System.TimeSpan.MinutesPerHour System.TimeSpan.Multiply(System.Double) System.TimeSpan.NanosecondsPerTick System.TimeSpan.Negate @@ -6576,6 +6835,9 @@ System.TimeSpan.ParseExact(System.String,System.String,System.IFormatProvider) System.TimeSpan.ParseExact(System.String,System.String,System.IFormatProvider,System.Globalization.TimeSpanStyles) System.TimeSpan.ParseExact(System.String,System.String[],System.IFormatProvider) System.TimeSpan.ParseExact(System.String,System.String[],System.IFormatProvider,System.Globalization.TimeSpanStyles) +System.TimeSpan.SecondsPerDay +System.TimeSpan.SecondsPerHour +System.TimeSpan.SecondsPerMinute System.TimeSpan.Subtract(System.TimeSpan) System.TimeSpan.TicksPerDay System.TimeSpan.TicksPerHour @@ -6931,6 +7193,7 @@ System.TypedReference.TargetTypeToken(System.TypedReference) System.TypedReference.ToObject(System.TypedReference) System.UInt128 System.UInt128.#ctor(System.UInt64,System.UInt64) +System.UInt128.BigMul(System.UInt128,System.UInt128,System.UInt128@) System.UInt128.Clamp(System.UInt128,System.UInt128,System.UInt128) System.UInt128.CompareTo(System.Object) System.UInt128.CompareTo(System.UInt128) @@ -7108,6 +7371,7 @@ System.UInt16.TryParse(System.String,System.Globalization.NumberStyles,System.IF System.UInt16.TryParse(System.String,System.IFormatProvider,System.UInt16@) System.UInt16.TryParse(System.String,System.UInt16@) System.UInt32 +System.UInt32.BigMul(System.UInt32,System.UInt32) System.UInt32.Clamp(System.UInt32,System.UInt32,System.UInt32) System.UInt32.CompareTo(System.Object) System.UInt32.CompareTo(System.UInt32) @@ -7157,6 +7421,7 @@ System.UInt32.TryParse(System.String,System.Globalization.NumberStyles,System.IF System.UInt32.TryParse(System.String,System.IFormatProvider,System.UInt32@) System.UInt32.TryParse(System.String,System.UInt32@) System.UInt64 +System.UInt64.BigMul(System.UInt64,System.UInt64) System.UInt64.Clamp(System.UInt64,System.UInt64,System.UInt64) System.UInt64.CompareTo(System.Object) System.UInt64.CompareTo(System.UInt64) @@ -7210,6 +7475,7 @@ System.UIntPtr.#ctor(System.UInt32) System.UIntPtr.#ctor(System.UInt64) System.UIntPtr.#ctor(System.Void*) System.UIntPtr.Add(System.UIntPtr,System.Int32) +System.UIntPtr.BigMul(System.UIntPtr,System.UIntPtr,System.UIntPtr@) System.UIntPtr.Clamp(System.UIntPtr,System.UIntPtr,System.UIntPtr) System.UIntPtr.CompareTo(System.Object) System.UIntPtr.CompareTo(System.UIntPtr) @@ -7302,7 +7568,9 @@ System.Uri.CheckSchemeName(System.String) System.Uri.CheckSecurity System.Uri.Compare(System.Uri,System.Uri,System.UriComponents,System.UriFormat,System.StringComparison) System.Uri.Equals(System.Object) +System.Uri.Equals(System.Uri) System.Uri.Escape +System.Uri.EscapeDataString(System.ReadOnlySpan{System.Char}) System.Uri.EscapeDataString(System.String) System.Uri.EscapeString(System.String) System.Uri.EscapeUriString(System.String) @@ -7330,8 +7598,11 @@ System.Uri.TryCreate(System.String,System.UriCreationOptions@,System.Uri@) System.Uri.TryCreate(System.String,System.UriKind,System.Uri@) System.Uri.TryCreate(System.Uri,System.String,System.Uri@) System.Uri.TryCreate(System.Uri,System.Uri,System.Uri@) +System.Uri.TryEscapeDataString(System.ReadOnlySpan{System.Char},System.Span{System.Char},System.Int32@) System.Uri.TryFormat(System.Span{System.Char},System.Int32@) +System.Uri.TryUnescapeDataString(System.ReadOnlySpan{System.Char},System.Span{System.Char},System.Int32@) System.Uri.Unescape(System.String) +System.Uri.UnescapeDataString(System.ReadOnlySpan{System.Char}) System.Uri.UnescapeDataString(System.String) System.Uri.UriSchemeFile System.Uri.UriSchemeFtp @@ -7581,6 +7852,7 @@ System.Version.CompareTo(System.Version) System.Version.Equals(System.Object) System.Version.Equals(System.Version) System.Version.GetHashCode +System.Version.Parse(System.ReadOnlySpan{System.Byte}) System.Version.Parse(System.ReadOnlySpan{System.Char}) System.Version.Parse(System.String) System.Version.ToString @@ -7589,6 +7861,7 @@ System.Version.TryFormat(System.Span{System.Byte},System.Int32,System.Int32@) System.Version.TryFormat(System.Span{System.Byte},System.Int32@) System.Version.TryFormat(System.Span{System.Char},System.Int32,System.Int32@) System.Version.TryFormat(System.Span{System.Char},System.Int32@) +System.Version.TryParse(System.ReadOnlySpan{System.Byte},System.Version@) System.Version.TryParse(System.ReadOnlySpan{System.Char},System.Version@) System.Version.TryParse(System.String,System.Version@) System.Version.get_Build diff --git a/src/Tools/Source/CompilerGeneratorTools/Source/BoundTreeGenerator/BoundNodeClassWriter.cs b/src/Tools/Source/CompilerGeneratorTools/Source/BoundTreeGenerator/BoundNodeClassWriter.cs index 2a7350e0d82d..133a645659fe 100644 --- a/src/Tools/Source/CompilerGeneratorTools/Source/BoundTreeGenerator/BoundNodeClassWriter.cs +++ b/src/Tools/Source/CompilerGeneratorTools/Source/BoundTreeGenerator/BoundNodeClassWriter.cs @@ -957,7 +957,7 @@ private void WriteUpdateMethod(Node node) private void WriteUpdatedMethodCSharp(Node node, bool emitNew) { Blank(); - Write("public{1} {0} Update", node.Name, emitNew ? " new" : ""); + Write("public{1} {0} Update", node.Name, (emitNew ? " new" : "") + (string.IsNullOrEmpty(node.UpdateMethodModifiers) ? "" : (" " + node.UpdateMethodModifiers))); Paren(); Comma(AllSpecifiableFields(node), field => string.Format("{0} {1}", GetField(node, field.Name).Type, ToCamelCase(field.Name))); UnParen(); @@ -1006,7 +1006,7 @@ string notEquals(Field field) private void WriteUpdatedMethodVB(Node node, bool emitNew) { Blank(); - Write("Public{0} Function Update", emitNew ? " Shadows" : ""); + Write("Public{0} Function Update", (emitNew ? " Shadows" : "") + (string.IsNullOrEmpty(node.UpdateMethodModifiers) ? "" : (" " + node.UpdateMethodModifiers))); Paren(); Comma(AllSpecifiableFields(node), field => string.Format("{1} As {0}", field.Type, ToCamelCase(field.Name))); UnParen(); diff --git a/src/Tools/Source/CompilerGeneratorTools/Source/BoundTreeGenerator/Model.cs b/src/Tools/Source/CompilerGeneratorTools/Source/BoundTreeGenerator/Model.cs index c026ddf4e3d4..d17f564a534c 100644 --- a/src/Tools/Source/CompilerGeneratorTools/Source/BoundTreeGenerator/Model.cs +++ b/src/Tools/Source/CompilerGeneratorTools/Source/BoundTreeGenerator/Model.cs @@ -33,6 +33,9 @@ public class TreeType [XmlAttribute] public string HasValidate; + + [XmlAttribute] + public string UpdateMethodModifiers; } public class PredefinedNode : TreeType diff --git a/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/Field.cs b/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/Field.cs index dcdb02097f0c..4d5ac9e67ee8 100644 --- a/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/Field.cs +++ b/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/Field.cs @@ -44,6 +44,9 @@ public class Field : TreeTypeChild [XmlAttribute] public string Type; + [XmlAttribute] + public string ExperimentalUrl; + [XmlAttribute] public string Optional; diff --git a/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/Kind.cs b/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/Kind.cs index ccc852b750c9..73a935c044c8 100644 --- a/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/Kind.cs +++ b/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/Kind.cs @@ -12,6 +12,9 @@ public class Kind : IEquatable [XmlAttribute] public string? Name; + [XmlAttribute] + public string? ExperimentalUrl; + public override bool Equals(object? obj) => Equals(obj as Kind); diff --git a/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/TreeType.cs b/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/TreeType.cs index fce0006a516c..a0910b3b0ab6 100644 --- a/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/TreeType.cs +++ b/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/Model/TreeType.cs @@ -17,6 +17,9 @@ public class TreeType [XmlAttribute] public string Base; + [XmlAttribute] + public string ExperimentalUrl; + [XmlAttribute] public string SkipConvenienceFactories; diff --git a/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/SourceWriter.cs b/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/SourceWriter.cs index 495b00d5d6af..600896f30ab1 100644 --- a/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/SourceWriter.cs +++ b/src/Tools/Source/CompilerGeneratorTools/Source/CSharpSyntaxGenerator/SourceWriter.cs @@ -26,6 +26,45 @@ private SourceWriter(TextWriter writer, Tree tree, CancellationToken cancellatio public static void WriteSyntax(TextWriter writer, Tree tree, CancellationToken cancellationToken = default) => new SourceWriter(writer, tree, cancellationToken).WriteSyntax(); + private static string QuoteString(string value) + => "@\"" + value.Replace("\"", "\"\"") + "\""; + + private void WriteExperimentalIfNeeded(string experimentalUrl) + { + if (!string.IsNullOrEmpty(experimentalUrl)) + { + WriteLine($"[Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = {QuoteString(experimentalUrl)})]"); + } + } + + private static string GetCreationExperimentalUrl(Node node) + { + if (!string.IsNullOrEmpty(node.ExperimentalUrl)) + { + return node.ExperimentalUrl; + } + + if (node.Kinds.Count <= 1) + { + return null; + } + + var experimentalKindCount = 0; + string experimentalUrl = null; + foreach (var kind in node.Kinds) + { + if (!string.IsNullOrEmpty(kind.ExperimentalUrl)) + { + experimentalKindCount++; + experimentalUrl ??= kind.ExperimentalUrl; + } + } + + // We use this to flag the transitional factory shape introduced when adding a new + // syntax kind, while avoiding broad experimental annotation of long-standing APIs. + return experimentalKindCount == node.Kinds.Count - 1 ? experimentalUrl : null; + } + private void WriteFileHeader() { WriteLine("// "); @@ -702,6 +741,7 @@ private void WriteRedType(TreeType node) if (node is AbstractNode) { var nd = (AbstractNode)node; + WriteExperimentalIfNeeded(node.ExperimentalUrl); WriteLine($"public abstract partial class {node.Name} : {node.Base}"); OpenBlock(); WriteLine($"internal {node.Name}(InternalSyntax.CSharpSyntaxNode green, SyntaxNode? parent, int position)"); @@ -720,6 +760,7 @@ private void WriteRedType(TreeType node) var fieldType = GetRedFieldType(field); WriteLine(); WriteComment(field.PropertyComment, ""); + WriteExperimentalIfNeeded(field.ExperimentalUrl); WriteLine($"{"public"} abstract {(IsNew(field) ? "new " : "")}{fieldType} {field.Name} {{ get; }}"); WriteLine($"public {node.Name} With{field.Name}({fieldType} {CamelCase(field.Name)}) => With{field.Name}Core({CamelCase(field.Name)});"); WriteLine($"internal abstract {node.Name} With{field.Name}Core({fieldType} {CamelCase(field.Name)});"); @@ -756,6 +797,7 @@ private void WriteRedType(TreeType node) { WriteLine(); WriteComment(field.PropertyComment, ""); + WriteExperimentalIfNeeded(field.ExperimentalUrl); WriteLine($"{"public"} abstract {(IsNew(field) ? "new " : "")}{field.Type} {field.Name} {{ get; }}"); } @@ -818,6 +860,7 @@ private void WriteRedType(TreeType node) WriteComment($""); WriteComment($""); + WriteExperimentalIfNeeded(node.ExperimentalUrl); WriteLine($"public sealed partial class {node.Name} : {node.Base}"); OpenBlock(); @@ -869,6 +912,7 @@ private void WriteRedType(TreeType node) if (field.Type == "SyntaxToken") { WriteComment(field.PropertyComment, ""); + WriteExperimentalIfNeeded(field.ExperimentalUrl); Write($"public {OverrideOrNewModifier(field)}{GetRedPropertyType(field)} {field.Name}"); if (IsOptional(field)) { @@ -889,6 +933,7 @@ private void WriteRedType(TreeType node) else if (field.Type == "SyntaxList") { WriteComment(field.PropertyComment, ""); + WriteExperimentalIfNeeded(field.ExperimentalUrl); WriteLine($"public {OverrideOrNewModifier(field)}SyntaxTokenList {field.Name}"); OpenBlock(); WriteLine("get"); @@ -901,6 +946,7 @@ private void WriteRedType(TreeType node) else { WriteComment(field.PropertyComment, ""); + WriteExperimentalIfNeeded(field.ExperimentalUrl); Write($"public {OverrideOrNewModifier(field)}{GetRedPropertyType(field)} {field.Name}"); if (IsNodeList(field.Type)) @@ -942,6 +988,7 @@ private void WriteRedType(TreeType node) foreach (var field in valueFields) { WriteComment(field.PropertyComment, ""); + WriteExperimentalIfNeeded(field.ExperimentalUrl); WriteLine($"{"public"} {OverrideOrNewModifier(field)}{field.Type} {field.Name} => ((InternalSyntax.{node.Name})this.Green).{field.Name};"); WriteLine(); } @@ -1088,6 +1135,7 @@ private void WriteRedVisitor(bool genericResult) WriteLine(); nWritten++; WriteComment($"Called when the visitor visits a {node.Name} node."); + WriteExperimentalIfNeeded(node.ExperimentalUrl); WriteLine($"public virtual {(genericResult ? "TResult?" : "void")} Visit{StripPost(node.Name, "Syntax")}({node.Name} node) => this.DefaultVisit(node);"); } CloseBlock(); @@ -1161,6 +1209,7 @@ private void WriteRedWithMethods(Node node) } } + WriteExperimentalIfNeeded(field.ExperimentalUrl); Write( $"public{(isNew ? " new " : " ")}{node.Name} With{StripPost(field.Name, "Opt")}({type} {CamelCase(field.Name)})" + " => Update("); @@ -1262,6 +1311,7 @@ private void WriteRedListHelperMethods(Node node, Field field) } } + WriteExperimentalIfNeeded(field.ExperimentalUrl); WriteLine($"public{(isNew ? " new " : " ")}{node.Name} Add{field.Name}(params {argType}[] items) => With{StripPost(field.Name, "Opt")}(this.{field.Name}.AddRange(items));"); } @@ -1281,6 +1331,7 @@ private void WriteRedNestedListHelperMethods(Node node, Field field, Node refere } // AddBaseListTypes + WriteExperimentalIfNeeded(field.ExperimentalUrl); Write($"public{(isNew ? " new " : " ")}{node.Name} Add{StripPost(field.Name, "Opt")}{referencedNodeField.Name}(params {argType}[] items)"); if (IsOptional(field)) @@ -1313,6 +1364,7 @@ private void WriteRedRewriter() if (nWritten > 0) WriteLine(); nWritten++; + WriteExperimentalIfNeeded(node.ExperimentalUrl); WriteLine($"public override SyntaxNode? Visit{StripPost(node.Name, "Syntax")}({node.Name} node)"); if (node.Fields.Count == 0) @@ -1439,6 +1491,7 @@ private void WriteRedFactory(Node nd) WriteComment($"Creates a new {nd.Name} instance."); + WriteExperimentalIfNeeded(GetCreationExperimentalUrl(nd)); Write($"public static {nd.Name} {StripPost(nd.Name, "Syntax")}("); WriteRedFactoryParameters(nd); @@ -1624,6 +1677,7 @@ private void WriteRedFactoryWithNoAutoCreatableTokens(Node nd) this.WriteLine(); WriteComment($"Creates a new {nd.Name} instance."); + WriteExperimentalIfNeeded(GetCreationExperimentalUrl(nd)); Write($"public static {nd.Name} {StripPost(nd.Name, "Syntax")}("); Write(CommaJoin( nd.Kinds.Count > 1 ? "SyntaxKind kind" : "", @@ -1713,6 +1767,7 @@ private void WriteRedMinimalFactory(Node nd, bool withStringNames = false) } WriteComment($"Creates a new {nd.Name} instance."); + WriteExperimentalIfNeeded(GetCreationExperimentalUrl(nd)); Write($"public static {nd.Name} {StripPost(nd.Name, "Syntax")}("); Write(CommaJoin( nd.Kinds.Count > 1 ? "SyntaxKind kind" : "", diff --git a/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/IOperationClassWriter.Verifier.cs b/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/IOperationClassWriter.Verifier.cs index 45c1562960f9..56d9cf9489b0 100644 --- a/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/IOperationClassWriter.Verifier.cs +++ b/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/IOperationClassWriter.Verifier.cs @@ -33,6 +33,12 @@ private bool ModelHasErrors(Tree tree) error = true; } + if (abstractNode.IsInternal && !string.IsNullOrEmpty(abstractNode.ExperimentalUrl)) + { + Console.WriteLine($"{abstractNode.Name} is marked as internal and experimental. Internal nodes cannot be experimental."); + error = true; + } + if (!abstractNode.IsInternal && abstractNode.Obsolete is null) { if (abstractNode.Comments?.Elements?[0].Name != "summary") @@ -43,6 +49,12 @@ private bool ModelHasErrors(Tree tree) foreach (var prop in abstractNode.Properties) { + if (prop.IsInternal && !string.IsNullOrEmpty(prop.ExperimentalUrl)) + { + Console.WriteLine($"{abstractNode.Name}.{prop.Name} is marked as internal and experimental. Internal properties cannot be experimental."); + error = true; + } + if (prop.Comments?.Elements?[0].Name != "summary" && !prop.IsInternal && !prop.IsOverride) { Console.WriteLine($"{abstractNode.Name}.{prop.Name} does not have correctly formatted comments, please ensure that there is a block for the property."); diff --git a/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/IOperationClassWriter.cs b/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/IOperationClassWriter.cs index 5f516ad7ed94..22bfb75f08b0 100644 --- a/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/IOperationClassWriter.cs +++ b/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/IOperationClassWriter.cs @@ -116,6 +116,7 @@ private bool WriteFiles() } WriteUsing("System.Collections.Immutable"); + WriteUsing("System.Diagnostics.CodeAnalysis"); if (@namespace != "Operations") { @@ -123,7 +124,6 @@ private bool WriteFiles() } else { - WriteUsing("System.Diagnostics.CodeAnalysis"); WriteUsing("Microsoft.CodeAnalysis.FlowAnalysis"); } @@ -163,6 +163,7 @@ private bool WriteFiles() writeHeader(); WriteUsing("System"); WriteUsing("System.ComponentModel"); + WriteUsing("System.Diagnostics.CodeAnalysis"); WriteUsing("Microsoft.CodeAnalysis.FlowAnalysis"); WriteUsing("Microsoft.CodeAnalysis.Operations"); @@ -205,6 +206,7 @@ private void WriteInterface(AbstractNode node) { WriteComments(node.Comments, getNodeKinds(node), writeReservedRemark: true); + WriteExperimentalAttributeIfNeeded(node); WriteObsoleteIfNecessary(node.Obsolete); WriteLine($"{(node.IsInternal ? "internal" : "public")} interface {node.Name} : {node.Base}"); Brace(); @@ -295,6 +297,7 @@ private void WriteInterfaceProperty(Property prop) if (prop.IsInternal || prop.IsOverride) return; WriteComments(prop.Comments, operationKinds: Enumerable.Empty(), writeReservedRemark: false); + WriteExperimentalAttributeIfNeeded(prop); var modifiers = prop.IsNew ? "new " : ""; WriteLine($"{modifiers}{prop.Type} {prop.Name} {{ get; }}"); } @@ -350,7 +353,8 @@ private void WriteOperationKind() entry.ExtraDescription, entry.EditorBrowsable ?? true, node.Obsolete?.Message, - node.Obsolete?.ErrorText); + node.Obsolete?.ErrorText, + experimentalUrl: node.ExperimentalUrl); } } else @@ -362,14 +366,15 @@ private void WriteOperationKind() currentEntry.OperationKind?.ExtraDescription, editorBrowsable: true, currentEntry.Obsolete?.Message, - currentEntry.Obsolete?.ErrorText); + currentEntry.Obsolete?.ErrorText, + experimentalUrl: currentEntry.ExperimentalUrl); Debug.Assert(elementsToKindEnumerator.MoveNext() || i == numKinds); } } Unbrace(); - void writeEnumElement(string kind, int value, string operationName, string? extraText, bool editorBrowsable, string? obsoleteMessage, string? obsoleteError) + void writeEnumElement(string kind, int value, string operationName, string? extraText, bool editorBrowsable, string? obsoleteMessage, string? obsoleteError, string? experimentalUrl) { WriteLine($"/// Indicates an .{(extraText is object ? $" {extraText}" : "")}"); @@ -378,6 +383,11 @@ void writeEnumElement(string kind, int value, string operationName, string? extr WriteLine("[EditorBrowsable(EditorBrowsableState.Never)]"); } + if (!string.IsNullOrEmpty(experimentalUrl)) + { + WriteExperimentalAttribute(experimentalUrl); + } + if (obsoleteMessage is object) { WriteLine($"[Obsolete({obsoleteMessage}, error: {obsoleteError})]"); @@ -1029,6 +1039,7 @@ public virtual void DefaultVisit(IOperation operation) { /* no-op */ } if (type.SkipInVisitor) continue; + WriteExperimentalAttributeIfNeeded(type); WriteObsoleteIfNecessary(type.Obsolete); var accessibility = type.IsInternal ? "internal" : "public"; var baseName = GetSubName(type.Name); @@ -1049,6 +1060,7 @@ public virtual void DefaultVisit(IOperation operation) { /* no-op */ } if (type.SkipInVisitor) continue; + WriteExperimentalAttributeIfNeeded(type); WriteObsoleteIfNecessary(type.Obsolete); var accessibility = type.IsInternal ? "internal" : "public"; WriteLine($"{accessibility} virtual TResult? {GetVisitorName(type)}({type.Name} operation, TArgument argument) => DefaultVisit(operation, argument);"); @@ -1066,6 +1078,27 @@ private void WriteObsoleteIfNecessary(ObsoleteTag? tag) } } + private void WriteExperimentalAttribute(string experimentalUrl) + { + WriteLine($"[Experimental(global::Microsoft.CodeAnalysis.RoslynExperiments.PreviewLanguageFeatureApi, UrlFormat = @\"{experimentalUrl.Replace("\"", "\"\"")}\")]"); + } + + private void WriteExperimentalAttributeIfNeeded(TreeType node) + { + if (!string.IsNullOrEmpty(node.ExperimentalUrl)) + { + WriteExperimentalAttribute(node.ExperimentalUrl); + } + } + + private void WriteExperimentalAttributeIfNeeded(Property prop) + { + if (!prop.IsOverride && !string.IsNullOrEmpty(prop.ExperimentalUrl)) + { + WriteExperimentalAttribute(prop.ExperimentalUrl); + } + } + private string GetVisitorName(Node type) { return type.VisitorName ?? $"Visit{GetSubName(type.Name)}"; diff --git a/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/Model.cs b/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/Model.cs index 0ea483c917c8..426f09dafa32 100644 --- a/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/Model.cs +++ b/src/Tools/Source/CompilerGeneratorTools/Source/IOperationGenerator/Model.cs @@ -34,6 +34,9 @@ public class TreeType [XmlAttribute] public string Base; + [XmlAttribute] + public string? ExperimentalUrl; + [XmlAttribute] public string? Namespace; @@ -109,6 +112,9 @@ public class Property [XmlAttribute] public string Type; + [XmlAttribute] + public string? ExperimentalUrl; + [XmlAttribute(AttributeName = "New")] public string NewText; public bool IsNew => NewText == "true"; diff --git a/src/Tools/Source/RunTests/Program.cs b/src/Tools/Source/RunTests/Program.cs index de4ee7e2f1df..76428d41d51d 100644 --- a/src/Tools/Source/RunTests/Program.cs +++ b/src/Tools/Source/RunTests/Program.cs @@ -371,11 +371,26 @@ static bool shouldExclude(string name, Options options) static bool IsMatch(TestRuntime testRuntime, string dirName) => testRuntime switch { - TestRuntime.Both => true, - TestRuntime.Core => Regex.IsMatch(dirName, @"^net\d+\."), + TestRuntime.Both => IsCompatibleWithCurrentPlatform(dirName), + TestRuntime.Core => Regex.IsMatch(dirName, @"^net\d+\.") && IsCompatibleWithCurrentPlatform(dirName), TestRuntime.Framework => dirName is "net472", _ => throw new InvalidOperationException($"Unexpected {nameof(TestRuntime)} value: {testRuntime}"), }; + + static bool IsCompatibleWithCurrentPlatform(string tfmDirName) + { + if (tfmDirName.EndsWith("-windows", StringComparison.Ordinal)) + { + return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + } + + if (tfmDirName.EndsWith("-macos", StringComparison.Ordinal)) + { + return RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + } + + return true; + } } private static void DisplayResults(Display display, ImmutableArray testResults) diff --git a/src/VisualStudio/CSharp/Impl/CSharpPackage.cs b/src/VisualStudio/CSharp/Impl/CSharpPackage.cs index d085fa72391e..b6cea2b578e0 100644 --- a/src/VisualStudio/CSharp/Impl/CSharpPackage.cs +++ b/src/VisualStudio/CSharp/Impl/CSharpPackage.cs @@ -98,11 +98,9 @@ protected override void RegisterObjectBrowserLibraryManager() { Contract.ThrowIfFalse(JoinableTaskFactory.Context.IsOnMainThread); - var workspace = this.ComponentModel.GetService(); - if (GetService(typeof(SVsObjectManager)) is IVsObjectManager2 objectManager) { - _libraryManager = new ObjectBrowserLibraryManager(this, ComponentModel, workspace); + _libraryManager = new ObjectBrowserLibraryManager(this, ComponentModel); if (ErrorHandler.Failed(objectManager.RegisterSimpleLibrary(_libraryManager, out _libraryManagerCookie))) { diff --git a/src/VisualStudio/CSharp/Impl/ObjectBrowser/ObjectBrowserLibraryManager.cs b/src/VisualStudio/CSharp/Impl/ObjectBrowser/ObjectBrowserLibraryManager.cs index bf0edb729257..f82bcad89aa3 100644 --- a/src/VisualStudio/CSharp/Impl/ObjectBrowser/ObjectBrowserLibraryManager.cs +++ b/src/VisualStudio/CSharp/Impl/ObjectBrowser/ObjectBrowserLibraryManager.cs @@ -14,9 +14,8 @@ namespace Microsoft.VisualStudio.LanguageServices.CSharp.ObjectBrowser; internal sealed class ObjectBrowserLibraryManager( IServiceProvider serviceProvider, - IComponentModel componentModel, - VisualStudioWorkspace workspace) : AbstractObjectBrowserLibraryManager( - LanguageNames.CSharp, Guids.CSharpLibraryId, serviceProvider, componentModel, workspace) + IComponentModel componentModel) : AbstractObjectBrowserLibraryManager( + LanguageNames.CSharp, Guids.CSharpLibraryId, serviceProvider, componentModel) { internal override AbstractDescriptionBuilder CreateDescriptionBuilder( IVsObjectBrowserDescription3 description, diff --git a/src/VisualStudio/CSharp/Test/DocumentOutline/DocumentOutlineTests.cs b/src/VisualStudio/CSharp/Test/DocumentOutline/DocumentOutlineTests.cs index 885b7a45a7f3..803c3d96ae51 100644 --- a/src/VisualStudio/CSharp/Test/DocumentOutline/DocumentOutlineTests.cs +++ b/src/VisualStudio/CSharp/Test/DocumentOutline/DocumentOutlineTests.cs @@ -60,7 +60,7 @@ public DocumentOutlineTests(ITestOutputHelper testOutputHelper) : base(testOutpu await using var mocks = await CreateMocksAsync(testCode); var response = await DocumentOutlineViewModel.DocumentSymbolsRequestAsync( mocks.TextBuffer, mocks.LanguageServiceBrokerCallback, mocks.FilePath, CancellationToken.None); - AssertEx.NotNull(response.Value); + Assert.NotNull(response); var model = DocumentOutlineViewModel.CreateDocumentSymbolData(response.Value.response, response.Value.snapshot); var uiItems = DocumentOutlineViewModel.GetDocumentSymbolItemViewModels(SortOption.Location, model); diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/AdditionalPropertiesTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/AdditionalPropertiesTests.cs index 24a116b98cb6..d46f6321d8c1 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/AdditionalPropertiesTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/AdditionalPropertiesTests.cs @@ -26,15 +26,14 @@ public sealed class AdditionalPropertiesTests [WpfFact] public async Task SetProperty_RootNamespace_CPS() { - using (var environment = new TestEnvironment()) - using (var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test")) - { - Assert.Null(DefaultNamespaceOfSingleProject(environment)); + using var environment = new TestEnvironment(); + await using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); - var rootNamespace = "Foo.Bar"; - project.SetProperty(BuildPropertyNames.RootNamespace, rootNamespace); - Assert.Equal(rootNamespace, DefaultNamespaceOfSingleProject(environment)); - } + Assert.Null(DefaultNamespaceOfSingleProject(environment)); + + var rootNamespace = "Foo.Bar"; + project.SetProperty(BuildPropertyNames.RootNamespace, rootNamespace); + Assert.Equal(rootNamespace, DefaultNamespaceOfSingleProject(environment)); static string DefaultNamespaceOfSingleProject(TestEnvironment environment) => environment.Workspace.CurrentSolution.Projects.Single().DefaultNamespace; @@ -53,7 +52,7 @@ public async Task SetProperty_MaxSupportedLangVersion_CPS(LanguageVersion? maxSu const LanguageVersion attemptedVersion = LanguageVersion.CSharp8; using var environment = new TestEnvironment(typeof(CSharpParseOptionsChangingService)); - using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); var project = environment.Workspace.CurrentSolution.Projects.Single(); var oldParseOptions = (CSharpParseOptions)project.ParseOptions; @@ -80,7 +79,7 @@ public async Task SetProperty_MaxSupportedLangVersion_CPS_NotSet() const LanguageVersion attemptedVersion = LanguageVersion.CSharp8; using var environment = new TestEnvironment(typeof(CSharpParseOptionsChangingService)); - using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); var project = environment.Workspace.CurrentSolution.Projects.Single(); var oldParseOptions = (CSharpParseOptions)project.ParseOptions; @@ -124,7 +123,7 @@ public async Task SetProperty_RunAnalyzersAndRunAnalyzersDuringLiveAnalysis(stri async Task TestCPSProject() { using var environment = new TestEnvironment(); - using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); cpsProject.SetProperty(BuildPropertyNames.RunAnalyzers, runAnalyzers); cpsProject.SetProperty(BuildPropertyNames.RunAnalyzersDuringLiveAnalysis, runAnalyzersDuringLiveAnalysis); @@ -156,24 +155,23 @@ void TestLegacyProject() [WpfFact] public async Task SetProperty_CompilerGeneratedFilesOutputPath_CPS() { - using (var environment = new TestEnvironment()) - using (var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test")) - { - Assert.Null(environment.Workspace.CurrentSolution.Projects.Single().CompilationOutputInfo.GeneratedFilesOutputDirectory); + using var environment = new TestEnvironment(); + await using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); - // relative path is relative to the project dir: - project.SetProperty(BuildPropertyNames.CompilerGeneratedFilesOutputPath, "generated"); - AssertEx.AreEqual( - Path.Combine(Path.GetDirectoryName(project.ProjectFilePath), "generated"), - environment.Workspace.CurrentSolution.Projects.Single().CompilationOutputInfo.GeneratedFilesOutputDirectory); + Assert.Null(environment.Workspace.CurrentSolution.Projects.Single().CompilationOutputInfo.GeneratedFilesOutputDirectory); - var path = Path.Combine(TempRoot.Root, "generated"); - project.SetProperty(BuildPropertyNames.CompilerGeneratedFilesOutputPath, path); - AssertEx.AreEqual(path, environment.Workspace.CurrentSolution.Projects.Single().CompilationOutputInfo.GeneratedFilesOutputDirectory); + // relative path is relative to the project dir: + project.SetProperty(BuildPropertyNames.CompilerGeneratedFilesOutputPath, "generated"); + AssertEx.AreEqual( + Path.Combine(Path.GetDirectoryName(project.ProjectFilePath), "generated"), + environment.Workspace.CurrentSolution.Projects.Single().CompilationOutputInfo.GeneratedFilesOutputDirectory); - // empty path: - project.SetProperty(BuildPropertyNames.CompilerGeneratedFilesOutputPath, ""); - Assert.Null(environment.Workspace.CurrentSolution.Projects.Single().CompilationOutputInfo.GeneratedFilesOutputDirectory); - } + var path = Path.Combine(TempRoot.Root, "generated"); + project.SetProperty(BuildPropertyNames.CompilerGeneratedFilesOutputPath, path); + AssertEx.AreEqual(path, environment.Workspace.CurrentSolution.Projects.Single().CompilationOutputInfo.GeneratedFilesOutputDirectory); + + // empty path: + project.SetProperty(BuildPropertyNames.CompilerGeneratedFilesOutputPath, ""); + Assert.Null(environment.Workspace.CurrentSolution.Projects.Single().CompilationOutputInfo.GeneratedFilesOutputDirectory); } } diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/AnalyzersTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/AnalyzersTests.cs index 8de728c79ddf..e295f3ded403 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/AnalyzersTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/AnalyzersTests.cs @@ -30,7 +30,7 @@ public async Task RuleSet_GeneralOption_CPS() """); using var environment = new TestEnvironment(); - using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); var workspaceProject = environment.Workspace.CurrentSolution.Projects.Single(); var options = (CSharpCompilationOptions)workspaceProject.CompilationOptions; @@ -59,7 +59,7 @@ public async Task RuleSet_SpecificOptions_CPS() """); using var environment = new TestEnvironment(); - using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); // Verify SetRuleSetFile updates the ruleset. project.SetOptions([$"/ruleset:{ruleSetFile.Path}"]); @@ -76,7 +76,7 @@ public async Task RuleSet_PathCanBeFound() using var environment = new TestEnvironment(); ProjectId projectId; - using (var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test")) + await using (var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test")) { project.SetOptions([$"/ruleset:{ruleSetFile.Path}"]); diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/CSharpCompilerOptionsTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/CSharpCompilerOptionsTests.cs index f288546bbe9e..42161b9f7126 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/CSharpCompilerOptionsTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/CSharpCompilerOptionsTests.cs @@ -27,7 +27,7 @@ public sealed class CSharpCompilerOptionsTests : TestBase public async Task DocumentationModeSetToDiagnoseIfProducingDocFile_CPS() { using var environment = new TestEnvironment(); - using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", commandLineArguments: @"/doc:DocFile.xml"); + await using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", commandLineArguments: @"/doc:DocFile.xml"); var parseOptions = environment.Workspace.CurrentSolution.Projects.Single().ParseOptions; Assert.Equal(DocumentationMode.Diagnose, parseOptions.DocumentationMode); } @@ -36,7 +36,7 @@ public async Task DocumentationModeSetToDiagnoseIfProducingDocFile_CPS() public async Task DocumentationModeSetToParseIfNotProducingDocFile_CPS() { using var environment = new TestEnvironment(); - using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", commandLineArguments: @"/doc:"); + await using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", commandLineArguments: @"/doc:"); var parseOptions = environment.Workspace.CurrentSolution.Projects.Single().ParseOptions; Assert.Equal(DocumentationMode.Parse, parseOptions.DocumentationMode); } @@ -45,7 +45,7 @@ public async Task DocumentationModeSetToParseIfNotProducingDocFile_CPS() public async Task ProjectSettingsOptionAddAndRemove_CPS() { using var environment = new TestEnvironment(); - using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", commandLineArguments: @"/warnaserror:CS1111"); + await using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", commandLineArguments: @"/warnaserror:CS1111"); var options = environment.GetUpdatedCompilationOptionOfSingleProject(); Assert.Equal(expected: ReportDiagnostic.Error, actual: options.SpecificDiagnosticOptions["CS1111"]); @@ -61,7 +61,7 @@ public async Task ProjectOutputBinPathChange_CPS() var initialBinPath = initialObjPath; using var environment = new TestEnvironment(); - using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", commandLineArguments: $"/out:{initialObjPath}"); + await using var project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", commandLineArguments: $"/out:{initialObjPath}"); Assert.Equal(initialObjPath, project.CompilationOutputAssemblyFilePath); Assert.Equal(initialBinPath, project.BinOutputPath); @@ -118,7 +118,7 @@ public async Task ProjectGuidSetter_CPS() var initialGuid = Guid.NewGuid(); using var environment = new TestEnvironment(); - using IWorkspaceProjectContext projectContext = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", initialGuid); + await using IWorkspaceProjectContext projectContext = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test", initialGuid); Assert.Equal(initialGuid, projectContext.Guid); var newGuid = Guid.NewGuid(); @@ -130,7 +130,7 @@ public async Task ProjectGuidSetter_CPS() public async Task ProjectLastDesignTimeBuildSucceededSetter_CPS() { using var environment = new TestEnvironment(); - using IWorkspaceProjectContext projectContext = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using IWorkspaceProjectContext projectContext = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); Assert.True(projectContext.LastDesignTimeBuildSucceeded); projectContext.LastDesignTimeBuildSucceeded = false; @@ -141,7 +141,7 @@ public async Task ProjectLastDesignTimeBuildSucceededSetter_CPS() public async Task ProjectDisplayNameSetter_CPS() { using var environment = new TestEnvironment(); - using IWorkspaceProjectContext project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using IWorkspaceProjectContext project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); Assert.Equal("Test", project.DisplayName); var initialProjectFilePath = project.ProjectFilePath; @@ -156,7 +156,7 @@ public async Task ProjectDisplayNameSetter_CPS() public async Task ProjectFilePathSetter_CPS() { using var environment = new TestEnvironment(); - using IWorkspaceProjectContext project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using IWorkspaceProjectContext project = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); var initialProjectDisplayName = project.DisplayName; var initialProjectFilePath = project.ProjectFilePath; var newFilePath = Temp.CreateFile().Path; @@ -178,7 +178,7 @@ public async Task ProjectFilePathSetter_CPS() public async Task ChecksumAlgorithm_CPS() { using var environment = new TestEnvironment(); - using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); Assert.Equal(SourceHashAlgorithms.Default, environment.Workspace.CurrentSolution.Projects.Single().State.ChecksumAlgorithm); @@ -191,7 +191,7 @@ public async Task ChecksumAlgorithm_CPS() public async Task CompilerGeneratedFilesOutputPath_CPS() { using var environment = new TestEnvironment(); - using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); + await using var cpsProject = await CSharpHelpers.CreateCSharpCPSProjectAsync(environment, "Test"); Assert.Null(environment.Workspace.CurrentSolution.Projects.Single().CompilationOutputInfo.GeneratedFilesOutputDirectory); diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/CSharpReferencesTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/CSharpReferencesTests.cs index cde3e6e6431b..3561811f2838 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/CSharpReferencesTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/CSharpReferencesTests.cs @@ -83,10 +83,10 @@ IEnumerable GetProject3MetadataReferences() Assert.False(GetProject3ProjectReferences().Any(pr => pr.ProjectId == project2.Id)); Assert.False(GetProject3MetadataReferences().Any(mr => mr.FilePath == metadaRefFilePath)); - project1.Dispose(); - project2.Dispose(); - project4.Dispose(); - project3.Dispose(); + await project1.DisposeAsync(); + await project2.DisposeAsync(); + await project4.DisposeAsync(); + await project3.DisposeAsync(); } [WpfFact] @@ -101,11 +101,11 @@ public async Task RemoveProjectConvertsProjectReferencesBack() Assert.Single(environment.Workspace.CurrentSolution.GetProject(project2.Id).AllProjectReferences); // Remove project1. project2's reference should have been converted back - project1.Dispose(); + await project1.DisposeAsync(); Assert.Empty(environment.Workspace.CurrentSolution.GetProject(project2.Id).AllProjectReferences); Assert.Single(environment.Workspace.CurrentSolution.GetProject(project2.Id).MetadataReferences); - project2.Dispose(); + await project2.DisposeAsync(); } [WpfFact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/461967")] @@ -121,15 +121,15 @@ public async Task AddingMetadataReferenceToProjectThatCannotCompileInTheIdeKeeps // We should not have converted that to a project reference, because we would have no way to produce the compilation Assert.Empty(environment.Workspace.CurrentSolution.GetProject(project1.Id).AllProjectReferences); - project2.Dispose(); - project1.Dispose(); + await project2.DisposeAsync(); + await project1.DisposeAsync(); } [WpfFact] public async Task AddRemoveAnalyzerReference_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); // Add analyzer reference using var tempRoot = new TempRoot(); var analyzerAssemblyFullPath = tempRoot.CreateFile().Path; diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/SourceFileHandlingTests.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/SourceFileHandlingTests.cs index 18e261ebc9f9..ed419c9e3b88 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/SourceFileHandlingTests.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CPS/SourceFileHandlingTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -27,7 +27,7 @@ public sealed class SourceFileHandlingTests public async Task AddRemoveSourceFile_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentDocuments() => environment.Workspace.CurrentSolution.Projects.Single().Documents; Assert.Empty(GetCurrentDocuments()); @@ -46,7 +46,7 @@ public async Task AddRemoveSourceFile_CPS() public async Task AddRemoveAdditionalFile_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentAdditionalDocuments() => environment.Workspace.CurrentSolution.Projects.Single().AdditionalDocuments; Assert.Empty(GetCurrentAdditionalDocuments()); @@ -64,7 +64,7 @@ public async Task AddRemoveAdditionalFile_CPS() public async Task ReorderSourceFiles_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentDocuments() => environment.Workspace.CurrentSolution.Projects.Single().Documents; VersionStamp GetVersion() => environment.Workspace.CurrentSolution.Projects.Single().Version; @@ -114,7 +114,7 @@ public async Task ReorderSourceFiles_CPS() public async Task ReorderSourceFilesBatch_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentDocuments() => environment.Workspace.CurrentSolution.Projects.Single().Documents; Assert.Empty(GetCurrentDocuments()); @@ -156,7 +156,7 @@ public async Task ReorderSourceFilesBatch_CPS() public async Task ReorderSourceFilesBatchWithReAdding_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentDocuments() => environment.Workspace.CurrentSolution.Projects.Single().Documents; Assert.Empty(GetCurrentDocuments()); @@ -210,7 +210,7 @@ public async Task ReorderSourceFilesBatchWithReAdding_CPS() public async Task ReorderSourceFilesBatchAddAfterReorder_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentDocuments() => environment.Workspace.CurrentSolution.Projects.Single().Documents; Assert.Empty(GetCurrentDocuments()); @@ -249,7 +249,7 @@ public async Task ReorderSourceFilesBatchAddAfterReorder_CPS() public async Task ReorderSourceFilesBatchRemoveAfterReorder_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentDocuments() => environment.Workspace.CurrentSolution.Projects.Single().Documents; Assert.Empty(GetCurrentDocuments()); @@ -288,7 +288,7 @@ public async Task ReorderSourceFilesBatchRemoveAfterReorder_CPS() public async Task ReorderSourceFilesExceptions_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentDocuments() => environment.Workspace.CurrentSolution.Projects.Single().Documents; Assert.Empty(GetCurrentDocuments()); @@ -322,7 +322,7 @@ public async Task ReorderSourceFilesExceptions_CPS() public async Task ReorderSourceFilesBatchExceptions_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentDocuments() => environment.Workspace.CurrentSolution.Projects.Single().Documents; Assert.Empty(GetCurrentDocuments()); @@ -368,7 +368,7 @@ public async Task ReorderSourceFilesBatchExceptions_CPS() public async Task ReorderSourceFilesBatchExceptionRemoveFile_CPS() { using var environment = new TestEnvironment(); - using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); + await using var project = await CreateCSharpCPSProjectAsync(environment, "project1"); IEnumerable GetCurrentDocuments() => environment.Workspace.CurrentSolution.Projects.Single().Documents; Assert.Empty(GetCurrentDocuments()); diff --git a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CSharpHelpers.cs b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CSharpHelpers.cs index 465933bf05fb..42a3c50f2888 100644 --- a/src/VisualStudio/CSharp/Test/ProjectSystemShim/CSharpHelpers.cs +++ b/src/VisualStudio/CSharp/Test/ProjectSystemShim/CSharpHelpers.cs @@ -15,7 +15,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.UnitTests; -using Microsoft.VisualStudio; using Microsoft.VisualStudio.LanguageServices.CSharp.ProjectSystemShim; using Microsoft.VisualStudio.LanguageServices.CSharp.ProjectSystemShim.Interop; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.CPS; @@ -29,22 +28,10 @@ namespace Roslyn.VisualStudio.CSharp.UnitTests.ProjectSystemShim; internal static class CSharpHelpers { public static CSharpProjectShim CreateCSharpProject(TestEnvironment environment, string projectName) - { - var projectBinPath = Path.GetTempPath(); - var hierarchy = environment.CreateHierarchy(projectName, projectBinPath, projectRefPath: null, projectCapabilities: "CSharp"); - - return CreateCSharpProject(environment, projectName, hierarchy); - } + => Microsoft.VisualStudio.LanguageServices.UnitTests.CSharpHelpers.CSharpHelpers.CreateCSharpProject(environment, projectName); public static CSharpProjectShim CreateCSharpProject(TestEnvironment environment, string projectName, IVsHierarchy hierarchy) - { - return new CSharpProjectShim( - new MockCSharpProjectRoot(hierarchy), - projectSystemName: projectName, - hierarchy: hierarchy, - serviceProvider: environment.ServiceProvider, - threadingContext: environment.ThreadingContext); - } + => Microsoft.VisualStudio.LanguageServices.UnitTests.CSharpHelpers.CSharpHelpers.CreateCSharpProject(environment, projectName, hierarchy); public static Task CreateCSharpCPSProjectAsync(TestEnvironment environment, string projectName, params string[] commandLineArguments) { @@ -139,80 +126,4 @@ public CommandLineArguments Parse(IEnumerable arguments, string baseDire return CSharpCommandLineParser.Default.Parse(arguments, baseDirectory, sdkDirectory); } } - - private sealed class MockCSharpProjectRoot : ICSharpProjectRoot - { - private readonly IVsHierarchy _hierarchy; - - public MockCSharpProjectRoot(IVsHierarchy hierarchy) - { - _hierarchy = hierarchy; - } - - int ICSharpProjectRoot.BelongsToProject(string pszFileName) - { - throw new NotImplementedException(); - } - - string ICSharpProjectRoot.BuildPerConfigCacheFileName() - { - throw new NotImplementedException(); - } - - bool ICSharpProjectRoot.CanCreateFileCodeModel(string pszFile) - { - throw new NotImplementedException(); - } - - void ICSharpProjectRoot.ConfigureCompiler(ICSCompiler compiler, ICSInputSet inputSet, bool addSources) - { - throw new NotImplementedException(); - } - - object ICSharpProjectRoot.CreateFileCodeModel(string pszFile, ref Guid riid) - { - throw new NotImplementedException(); - } - - string ICSharpProjectRoot.GetActiveConfigurationName() - { - throw new NotImplementedException(); - } - - string ICSharpProjectRoot.GetFullProjectName() - { - throw new NotImplementedException(); - } - - int ICSharpProjectRoot.GetHierarchyAndItemID(string pszFile, out IVsHierarchy ppHier, out uint pItemID) - { - ppHier = _hierarchy; - - // Each item should have it's own ItemID, but for simplicity we'll just hard-code a value of - // no particular significance. - pItemID = 42; - - return VSConstants.S_OK; - } - - void ICSharpProjectRoot.GetHierarchyAndItemIDOptionallyInProject(string pszFile, out IVsHierarchy ppHier, out uint pItemID, bool mustBeInProject) - { - throw new NotImplementedException(); - } - - string ICSharpProjectRoot.GetProjectLocation() - { - throw new NotImplementedException(); - } - - object ICSharpProjectRoot.GetProjectSite(ref Guid riid) - { - throw new NotImplementedException(); - } - - void ICSharpProjectRoot.SetProjectSite(ICSharpProjectSite site) - { - throw new NotImplementedException(); - } - } } diff --git a/src/VisualStudio/Core/Def/CallHierarchy/CallHierarchyItem.cs b/src/VisualStudio/Core/Def/CallHierarchy/CallHierarchyItem.cs index 3dcf52c55c11..9d936b09b2ee 100644 --- a/src/VisualStudio/Core/Def/CallHierarchy/CallHierarchyItem.cs +++ b/src/VisualStudio/Core/Def/CallHierarchy/CallHierarchyItem.cs @@ -8,76 +8,59 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Media; -using Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; +using Microsoft.CodeAnalysis.CallHierarchy; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Navigation; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.VisualStudio.Language.CallHierarchy; using Microsoft.VisualStudio.LanguageServices; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy; +internal readonly record struct CallHierarchySearchCategoryEntry( + CallHierarchySearchDescriptor SearchDescriptor, + string SearchCategory, + string DisplayName); + internal sealed class CallHierarchyItem : ICallHierarchyMemberItem { private readonly Workspace _workspace; private readonly INavigableLocation _navigableLocation; private readonly ImmutableArray _callsites; - private readonly ImmutableArray _finders; + private readonly ImmutableArray _searchCategories; + private readonly Dictionary _searches = []; private readonly Func _glyphCreator; private readonly CallHierarchyProvider _provider; + private readonly object _gate = new(); + public CallHierarchyItem( CallHierarchyProvider provider, - ISymbol symbol, + CallHierarchyItemDescriptor descriptor, INavigableLocation navigableLocation, - ImmutableArray finders, + ImmutableArray searchCategories, Func glyphCreator, + string sortText, + string projectName, ImmutableArray callsites, Project project) { _workspace = project.Solution.Workspace; _provider = provider; _navigableLocation = navigableLocation; - _finders = finders; - ContainingTypeName = symbol.ContainingType.ToDisplayString(ContainingTypeFormat); - ContainingNamespaceName = symbol.ContainingNamespace.ToDisplayString(ContainingNamespaceFormat); + _searchCategories = searchCategories; + ContainingTypeName = descriptor.ContainingTypeName; + ContainingNamespaceName = descriptor.ContainingNamespaceName; _glyphCreator = glyphCreator; - MemberName = symbol.ToDisplayString(MemberNameFormat); + MemberName = descriptor.MemberName; _callsites = callsites.SelectAsArray(loc => new CallHierarchyDetail(provider, loc, _workspace)); - SortText = symbol.ToDisplayString(); - ProjectName = project.Name; + SortText = sortText; + ProjectName = projectName; } - public static readonly SymbolDisplayFormat MemberNameFormat = - new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces, - propertyStyle: SymbolDisplayPropertyStyle.NameOnly, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - memberOptions: SymbolDisplayMemberOptions.IncludeParameters | SymbolDisplayMemberOptions.IncludeExplicitInterface, - parameterOptions: - SymbolDisplayParameterOptions.IncludeParamsRefOut | - SymbolDisplayParameterOptions.IncludeExtensionThis | - SymbolDisplayParameterOptions.IncludeType, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - public static readonly SymbolDisplayFormat ContainingTypeFormat = - new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes, - genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, - miscellaneousOptions: - SymbolDisplayMiscellaneousOptions.UseSpecialTypes); - - public static readonly SymbolDisplayFormat ContainingNamespaceFormat = - new( - globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted, - typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces); - public string ProjectName { get; } public string ContainingNamespaceName { get; } @@ -104,7 +87,7 @@ public IEnumerable SupportedSearchCategories { get { - return _finders.Select(s => new CallHierarchySearchCategory(s.SearchCategory, s.DisplayName)); + return _searchCategories.Select(s => new CallHierarchySearchCategory(s.SearchCategory, s.DisplayName)); } } @@ -118,8 +101,16 @@ public bool SupportsFindReferences public void CancelSearch(string categoryName) { - var finder = _finders.FirstOrDefault(s => s.SearchCategory == categoryName); - finder.CancelSearch(); + lock (_gate) + { + CancelSearch_NoLock(categoryName); + } + } + + private void CancelSearch_NoLock(string categoryName) + { + if (_searches.TryGetValue(categoryName, out var cancellationSource)) + cancellationSource.Cancel(); } public void FindReferences() @@ -145,10 +136,7 @@ await _navigableLocation.NavigateToAsync( } public void StartSearch(string categoryName, CallHierarchySearchScope searchScope, ICallHierarchySearchCallback callback) - { - var finder = _finders.FirstOrDefault(s => s.SearchCategory == categoryName); - finder.StartSearch(_workspace, searchScope, callback); - } + => StartSearchWorker(categoryName, searchScope, callback, documents: null); public void ResumeSearch(string categoryName) { @@ -163,9 +151,84 @@ public void SuspendSearch(string categoryName) // For Testing only internal void StartSearchWithDocuments(string categoryName, CallHierarchySearchScope searchScope, ICallHierarchySearchCallback callback, IImmutableSet documents) + => StartSearchWorker(categoryName, searchScope, callback, documents); + + /// + /// Starts a search operation for the given category. + /// + /// Threading guarantees: + /// - This method is called on the UI thread by VS. + /// - Concurrent calls with different categoryNames are safe; each category has its own cancellation source. + /// - Concurrent calls with the same categoryName will cancel the previous search. + /// - The actual search work is offloaded to a background thread via Task.Run to avoid blocking the UI thread. + /// - Callbacks are invoked on the background thread; the callback implementation must handle thread safety. + /// + private void StartSearchWorker(string categoryName, CallHierarchySearchScope searchScope, ICallHierarchySearchCallback callback, IImmutableSet documents) { - var finder = _finders.FirstOrDefault(s => s.SearchCategory == categoryName); - finder.SetDocuments(documents); - finder.StartSearch(_workspace, searchScope, callback); + var searchCategory = _searchCategories.FirstOrDefault(s => s.SearchCategory == categoryName); + if (searchCategory == default) + return; + + CancellationTokenSource cancellationSource; + lock (_gate) + { + CancelSearch_NoLock(categoryName); + + cancellationSource = new(); + _searches[categoryName] = cancellationSource; + } + + var asyncToken = _provider.AsyncListener.BeginAsyncOperation(this.GetType().Name + ".Search"); + + // NOTE: This task has CancellationToken.None specified, since it must complete no matter what + // so the callback is appropriately notified that the search has terminated. + Task.Run(async () => + { + string completionErrorMessage = null; + try + { + var results = await _provider.SearchAsync( + _workspace, searchCategory.SearchDescriptor, searchScope, documents, cancellationSource.Token).ConfigureAwait(false); + foreach (var result in results) + { + if (result.Item != null) + { + var item = await _provider.CreateItemAsync(result.Item, _workspace, result.ReferenceLocations, cancellationSource.Token).ConfigureAwait(false); + callback.AddResult(item); + } + else + { + var details = result.ReferenceLocations.SelectAsArray(loc => new CallHierarchyDetail(_provider, loc, _workspace)); + callback.AddResult(_provider.CreateInitializerItem(details)); + } + + cancellationSource.Token.ThrowIfCancellationRequested(); + } + } + catch (OperationCanceledException) + { + completionErrorMessage = EditorFeaturesResources.Canceled; + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { + completionErrorMessage = e.Message; + } + finally + { + lock (_gate) + { + _searches.Remove(categoryName); + } + + if (completionErrorMessage != null) + { + callback.SearchFailed(completionErrorMessage); + } + else + { + callback.SearchSucceeded(); + } + } + }, CancellationToken.None).CompletesAsyncOperation(asyncToken); } } diff --git a/src/VisualStudio/Core/Def/CallHierarchy/CallHierarchyProvider.cs b/src/VisualStudio/Core/Def/CallHierarchy/CallHierarchyProvider.cs index 30e91b88e335..609f37950455 100644 --- a/src/VisualStudio/Core/Def/CallHierarchy/CallHierarchyProvider.cs +++ b/src/VisualStudio/Core/Def/CallHierarchy/CallHierarchyProvider.cs @@ -6,18 +6,16 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.Composition; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CallHierarchy; using Microsoft.CodeAnalysis.Editor.Host; -using Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.GoToDefinition; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.Language.CallHierarchy; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; using Microsoft.VisualStudio.Utilities; @@ -53,39 +51,33 @@ public CallHierarchyProvider( public async Task CreateItemAsync( ISymbol symbol, Project project, ImmutableArray callsites, CancellationToken cancellationToken) { - if (symbol.Kind is SymbolKind.Method or - SymbolKind.Property or - SymbolKind.Event or - SymbolKind.Field) - { - symbol = GetTargetSymbol(symbol); - - var finders = await CreateFindersAsync(symbol, project, cancellationToken).ConfigureAwait(false); - var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( - symbol, project.Solution, this.ThreadingContext, _streamingPresenter.Value, cancellationToken).ConfigureAwait(false); - return new CallHierarchyItem( - this, - symbol, - location, - finders, - () => symbol.GetGlyph().GetImageSource(GlyphService), - callsites, - project); - } - - return null; + var service = project.GetRequiredLanguageService(); + var descriptor = await service.CreateItemAsync(symbol, project, cancellationToken).ConfigureAwait(false); + return descriptor != null + ? await CreateItemAsync(descriptor, project.Solution.Workspace, callsites, cancellationToken).ConfigureAwait(false) + : null; } - private static ISymbol GetTargetSymbol(ISymbol symbol) + public async Task CreateItemAsync( + CallHierarchyItemDescriptor descriptor, Workspace workspace, ImmutableArray callsites, CancellationToken cancellationToken) { - if (symbol is IMethodSymbol methodSymbol) - { - methodSymbol = methodSymbol.ReducedFrom ?? methodSymbol; - methodSymbol = methodSymbol.ConstructedFrom ?? methodSymbol; - return methodSymbol; - } - - return symbol; + var resolved = await descriptor.ItemId.TryResolveAsync(workspace.CurrentSolution, cancellationToken).ConfigureAwait(false); + if (resolved == null) + return null; + + var (symbol, project) = resolved.Value; + var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( + symbol, project.Solution, this.ThreadingContext, _streamingPresenter.Value, cancellationToken).ConfigureAwait(false); + return new CallHierarchyItem( + this, + descriptor, + location, + await CreateSearchCategoryEntriesAsync(descriptor, symbol, workspace.CurrentSolution, cancellationToken).ConfigureAwait(false), + () => descriptor.Glyph.GetImageSource(GlyphService), + symbol.ToDisplayString(), + project.Name, + callsites, + project); } public FieldInitializerItem CreateInitializerItem(IEnumerable details) @@ -96,50 +88,98 @@ public FieldInitializerItem CreateInitializerItem(IEnumerable> CreateFindersAsync(ISymbol symbol, Project project, CancellationToken cancellationToken) + public async Task> SearchAsync( + Workspace workspace, + CallHierarchySearchDescriptor searchDescriptor, + CallHierarchySearchScope searchScope, + IImmutableSet? documents, + CancellationToken cancellationToken) { - if (symbol.Kind is SymbolKind.Property or - SymbolKind.Event or - SymbolKind.Method) - { - var finders = new List - { - new MethodCallFinder(symbol, project.Id, AsyncListener, this) - }; + var project = workspace.CurrentSolution.GetProject(searchDescriptor.ItemId.ProjectId); + if (project == null) + return []; - if (symbol.IsVirtual || symbol.IsAbstract) - { - finders.Add(new OverridingMemberFinder(symbol, project.Id, AsyncListener, this)); - } + documents ??= IncludeDocuments(searchScope, project); + var service = project.GetRequiredLanguageService(); + return await service.SearchIncomingCallsAsync(workspace.CurrentSolution, searchDescriptor, documents, cancellationToken).ConfigureAwait(false); + } - var @overrides = await SymbolFinder.FindOverridesAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); - if (overrides.Any()) + private static IImmutableSet? IncludeDocuments(CallHierarchySearchScope scope, Project project) + { + if (scope is CallHierarchySearchScope.CurrentDocument or CallHierarchySearchScope.CurrentProject) + { + var documentTrackingService = project.Solution.Services.GetRequiredService(); + var activeDocument = documentTrackingService.TryGetActiveDocument(); + if (activeDocument != null) { - finders.Add(new CallToOverrideFinder(symbol, project.Id, AsyncListener, this)); + if (scope == CallHierarchySearchScope.CurrentProject) + { + var currentProject = project.Solution.GetProject(activeDocument.ProjectId); + if (currentProject != null) + return ImmutableHashSet.CreateRange(currentProject.Documents); + } + else + { + var currentDocument = project.Solution.GetDocument(activeDocument); + if (currentDocument != null) + return ImmutableHashSet.Create(currentDocument); + } + + return ImmutableHashSet.Empty; } + } - if (symbol.GetOverriddenMember() is ISymbol overridenMember) - { - finders.Add(new BaseMemberFinder(overridenMember, project.Id, AsyncListener, this)); - } + return null; + } - var implementedInterfaceMembers = await SymbolFinder.FindImplementedInterfaceMembersAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); - foreach (var implementedInterfaceMember in implementedInterfaceMembers) - { - finders.Add(new InterfaceImplementationCallFinder(implementedInterfaceMember, project.Id, AsyncListener, this)); - } + private static async Task> CreateSearchCategoryEntriesAsync( + CallHierarchyItemDescriptor descriptor, + ISymbol symbol, + Solution solution, + CancellationToken cancellationToken) + { + var builder = ImmutableArray.CreateBuilder(descriptor.SupportedSearchDescriptors.Length); + foreach (var searchDescriptor in descriptor.SupportedSearchDescriptors) + { + builder.Add(await CreateSearchCategoryEntryAsync(searchDescriptor, symbol, solution, cancellationToken).ConfigureAwait(false)); + } - if (symbol.IsImplementableMember()) - { - finders.Add(new ImplementerFinder(symbol, project.Id, AsyncListener, this)); - } + return builder.MoveToImmutable(); + } - return finders.ToImmutableArray(); - } + private static async Task CreateSearchCategoryEntryAsync( + CallHierarchySearchDescriptor descriptor, + ISymbol symbol, + Solution solution, + CancellationToken cancellationToken) + { + var relatedSymbol = descriptor.Relationship switch + { + CallHierarchyRelationshipKind.BaseMember or CallHierarchyRelationshipKind.InterfaceImplementations + => (await descriptor.ItemId.TryResolveAsync(solution, cancellationToken).ConfigureAwait(false))?.Symbol, + _ => null, + }; - if (symbol.Kind == SymbolKind.Field) - return [new FieldReferenceFinder(symbol, project.Id, AsyncListener, this)]; + var displayName = descriptor.Relationship switch + { + CallHierarchyRelationshipKind.Callers => string.Format(EditorFeaturesResources.Calls_To_0, symbol.Name), + CallHierarchyRelationshipKind.CallsToOverrides => EditorFeaturesResources.Calls_To_Overrides, + CallHierarchyRelationshipKind.BaseMember => string.Format(EditorFeaturesResources.Calls_To_Base_Member_0, relatedSymbol?.ToDisplayString() ?? symbol.ToDisplayString()), + CallHierarchyRelationshipKind.InterfaceImplementations => string.Format(EditorFeaturesResources.Calls_To_Interface_Implementation_0, relatedSymbol?.ToDisplayString() ?? symbol.ToDisplayString()), + CallHierarchyRelationshipKind.Implementations => string.Format(EditorFeaturesResources.Implements_0, symbol.Name), + CallHierarchyRelationshipKind.Overrides => EditorFeaturesResources.Overrides_, + CallHierarchyRelationshipKind.FieldReferences => string.Format(EditorFeaturesResources.References_To_Field_0, symbol.Name), + _ => throw new InvalidOperationException(), + }; + + var searchCategory = descriptor.Relationship switch + { + CallHierarchyRelationshipKind.Callers => CallHierarchyPredefinedSearchCategoryNames.Callers, + CallHierarchyRelationshipKind.InterfaceImplementations => CallHierarchyPredefinedSearchCategoryNames.InterfaceImplementations, + CallHierarchyRelationshipKind.Overrides => CallHierarchyPredefinedSearchCategoryNames.Overrides, + _ => displayName, + }; - return []; + return new CallHierarchySearchCategoryEntry(descriptor, searchCategory, displayName); } } diff --git a/src/VisualStudio/Core/Def/CallHierarchy/Finders/AbstractCallFinder.cs b/src/VisualStudio/Core/Def/CallHierarchy/Finders/AbstractCallFinder.cs deleted file mode 100644 index eb1295fa59ab..000000000000 --- a/src/VisualStudio/Core/Def/CallHierarchy/Finders/AbstractCallFinder.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#nullable disable - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.VisualStudio.Language.CallHierarchy; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; - -internal abstract class AbstractCallFinder -{ - private readonly IAsynchronousOperationListener _asyncListener; - private readonly CancellationTokenSource _cancellationSource = new(); - private readonly ProjectId _projectId; - private readonly SymbolKey _symbolKey; - - protected readonly CallHierarchyProvider Provider; - protected readonly string SymbolName; - - // For Testing only - internal IImmutableSet Documents; - - protected AbstractCallFinder( - ISymbol symbol, - ProjectId projectId, - IAsynchronousOperationListener asyncListener, - CallHierarchyProvider provider) - { - _asyncListener = asyncListener; - _symbolKey = symbol.GetSymbolKey(); - this.SymbolName = symbol.Name; - _projectId = projectId; - this.Provider = provider; - } - - internal void SetDocuments(IImmutableSet documents) - => this.Documents = documents; - - public abstract string DisplayName { get; } - - public virtual string SearchCategory => DisplayName; - - public void CancelSearch() - => _cancellationSource.Cancel(); - - public void StartSearch(Workspace workspace, CallHierarchySearchScope searchScope, ICallHierarchySearchCallback callback) - { - var asyncToken = _asyncListener.BeginAsyncOperation(this.GetType().Name + ".Search"); - - // NOTE: This task has CancellationToken.None specified, since it must complete no matter what - // so the callback is appropriately notified that the search has terminated. - Task.Run(async () => - { - // The error message to show if we had an error. null will mean we succeeded. - string completionErrorMessage = null; - try - { - await SearchAsync(workspace, searchScope, callback, _cancellationSource.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - completionErrorMessage = EditorFeaturesResources.Canceled; - } - catch (Exception e) when (FatalError.ReportAndCatch(e)) - { - completionErrorMessage = e.Message; - } - finally - { - if (completionErrorMessage != null) - { - callback.SearchFailed(completionErrorMessage); - } - else - { - callback.SearchSucceeded(); - } - } - }, CancellationToken.None).CompletesAsyncOperation(asyncToken); - } - - private async Task SearchAsync(Workspace workspace, CallHierarchySearchScope scope, ICallHierarchySearchCallback callback, CancellationToken cancellationToken) - { - var project = workspace.CurrentSolution.GetProject(_projectId); - - if (project == null) - { - throw new Exception(string.Format(WorkspacesResources.The_symbol_0_cannot_be_located_within_the_current_solution, SymbolName)); - } - - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - var resolution = _symbolKey.Resolve(compilation, cancellationToken: cancellationToken); - - var symbol = resolution.Symbol; - - if (symbol == null) - { - throw new Exception(string.Format(WorkspacesResources.The_symbol_0_cannot_be_located_within_the_current_solution, SymbolName)); - } - - var documents = this.Documents ?? IncludeDocuments(scope, project); - - await SearchWorkerAsync(symbol, project, callback, documents, cancellationToken).ConfigureAwait(false); - } - - private static IImmutableSet IncludeDocuments(CallHierarchySearchScope scope, Project project) - { - if (scope is CallHierarchySearchScope.CurrentDocument or CallHierarchySearchScope.CurrentProject) - { - var documentTrackingService = project.Solution.Services.GetRequiredService(); - var activeDocument = documentTrackingService.TryGetActiveDocument(); - if (activeDocument != null) - { - if (scope == CallHierarchySearchScope.CurrentProject) - { - var currentProject = project.Solution.GetProject(activeDocument.ProjectId); - if (currentProject != null) - { - return ImmutableHashSet.CreateRange(currentProject.Documents); - } - } - else - { - var currentDocument = project.Solution.GetDocument(activeDocument); - if (currentDocument != null) - { - return ImmutableHashSet.Create(currentDocument); - } - } - - return ImmutableHashSet.Empty; - } - } - - return null; - } - - protected virtual async Task SearchWorkerAsync(ISymbol symbol, Project project, ICallHierarchySearchCallback callback, IImmutableSet documents, CancellationToken cancellationToken) - { - var callers = await GetCallersAsync(symbol, project, documents, cancellationToken).ConfigureAwait(false); - - var initializerLocations = new List(); - - foreach (var caller in callers) - { - if (caller.IsDirect) - { - if (caller.CallingSymbol.Kind == SymbolKind.Field) - { - initializerLocations.AddRange(caller.Locations.Select( - loc => new CallHierarchyDetail(this.Provider, loc, project.Solution.Workspace))); - } - else - { - var callingProject = project.Solution.GetProject(caller.CallingSymbol.ContainingAssembly, cancellationToken); - var item = await Provider.CreateItemAsync(caller.CallingSymbol, callingProject, [.. caller.Locations], cancellationToken).ConfigureAwait(false); - callback.AddResult(item); - cancellationToken.ThrowIfCancellationRequested(); - } - } - } - - if (initializerLocations.Any()) - { - var initializerItem = Provider.CreateInitializerItem(initializerLocations); - callback.AddResult(initializerItem); - } - } - - protected abstract Task> GetCallersAsync(ISymbol symbol, Project project, IImmutableSet documents, CancellationToken cancellationToken); -} diff --git a/src/VisualStudio/Core/Def/CallHierarchy/Finders/BaseMemberFinder.cs b/src/VisualStudio/Core/Def/CallHierarchy/Finders/BaseMemberFinder.cs deleted file mode 100644 index 2f258d909989..000000000000 --- a/src/VisualStudio/Core/Def/CallHierarchy/Finders/BaseMemberFinder.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Shared.TestHooks; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; - -internal sealed class BaseMemberFinder : AbstractCallFinder -{ - private readonly string _text; - - public BaseMemberFinder(ISymbol symbol, ProjectId projectId, IAsynchronousOperationListener asyncListener, CallHierarchyProvider provider) - : base(symbol, projectId, asyncListener, provider) - { - _text = string.Format(EditorFeaturesResources.Calls_To_Base_Member_0, symbol.ToDisplayString()); - } - - public override string DisplayName => _text; - - protected override async Task> GetCallersAsync(ISymbol symbol, Project project, IImmutableSet documents, CancellationToken cancellationToken) - { - var calls = await SymbolFinder.FindCallersAsync(symbol, project.Solution, documents, cancellationToken).ConfigureAwait(false); - return calls.Where(c => c.IsDirect); - } -} diff --git a/src/VisualStudio/Core/Def/CallHierarchy/Finders/CallToOverrideFinder.cs b/src/VisualStudio/Core/Def/CallHierarchy/Finders/CallToOverrideFinder.cs deleted file mode 100644 index 87472a171f66..000000000000 --- a/src/VisualStudio/Core/Def/CallHierarchy/Finders/CallToOverrideFinder.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Shared.TestHooks; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; - -internal sealed class CallToOverrideFinder : AbstractCallFinder -{ - public CallToOverrideFinder(ISymbol symbol, ProjectId projectId, IAsynchronousOperationListener asyncListener, CallHierarchyProvider provider) - : base(symbol, projectId, asyncListener, provider) - { - } - - public override string DisplayName => EditorFeaturesResources.Calls_To_Overrides; - - protected override async Task> GetCallersAsync(ISymbol symbol, Project project, IImmutableSet documents, CancellationToken cancellationToken) - { - var overrides = await SymbolFinder.FindOverridesAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); - var callsToOverrides = new List(); - - foreach (var @override in overrides) - { - var calls = await SymbolFinder.FindCallersAsync(@override, project.Solution, documents, cancellationToken).ConfigureAwait(false); - - foreach (var call in calls) - { - if (call.IsDirect) - { - callsToOverrides.Add(call); - } - - cancellationToken.ThrowIfCancellationRequested(); - } - } - - return callsToOverrides; - } -} diff --git a/src/VisualStudio/Core/Def/CallHierarchy/Finders/FieldReferenceFinder.cs b/src/VisualStudio/Core/Def/CallHierarchy/Finders/FieldReferenceFinder.cs deleted file mode 100644 index 13e1c1d91e02..000000000000 --- a/src/VisualStudio/Core/Def/CallHierarchy/Finders/FieldReferenceFinder.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Shared.TestHooks; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; - -internal sealed class FieldReferenceFinder : AbstractCallFinder -{ - public FieldReferenceFinder(ISymbol symbol, ProjectId projectId, IAsynchronousOperationListener asyncListener, CallHierarchyProvider provider) - : base(symbol, projectId, asyncListener, provider) - { - } - - public override string DisplayName - { - get - { - return string.Format(EditorFeaturesResources.References_To_Field_0, SymbolName); - } - } - - protected override async Task> GetCallersAsync(ISymbol symbol, Project project, IImmutableSet documents, CancellationToken cancellationToken) - { - var callers = await SymbolFinder.FindCallersAsync(symbol, project.Solution, documents, cancellationToken).ConfigureAwait(false); - return callers.Where(c => c.IsDirect); - } -} diff --git a/src/VisualStudio/Core/Def/CallHierarchy/Finders/ImplementerFinder.cs b/src/VisualStudio/Core/Def/CallHierarchy/Finders/ImplementerFinder.cs deleted file mode 100644 index f886cd03efb4..000000000000 --- a/src/VisualStudio/Core/Def/CallHierarchy/Finders/ImplementerFinder.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.VisualStudio.Language.CallHierarchy; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; - -internal sealed class ImplementerFinder : AbstractCallFinder -{ - public ImplementerFinder(ISymbol symbol, ProjectId projectId, IAsynchronousOperationListener asyncListener, CallHierarchyProvider provider) - : base(symbol, projectId, asyncListener, provider) - { - } - - public override string DisplayName - { - get - { - return string.Format(EditorFeaturesResources.Implements_0, SymbolName); - } - } - - protected override Task> GetCallersAsync(ISymbol symbol, Project project, IImmutableSet documents, CancellationToken cancellationToken) - => throw new NotImplementedException(); - - protected override async Task SearchWorkerAsync(ISymbol symbol, Project project, ICallHierarchySearchCallback callback, IImmutableSet documents, CancellationToken cancellationToken) - { - var implementations = await SymbolFinder.FindImplementationsAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var implementation in implementations) - { - var sourceLocations = implementation.DeclaringSyntaxReferences.Select(d => project.Solution.GetDocument(d.SyntaxTree)).WhereNotNull(); - var bestLocation = sourceLocations.FirstOrDefault(d => documents == null || documents.Contains(d)); - if (bestLocation != null) - { - var item = await Provider.CreateItemAsync(implementation, bestLocation.Project, [], cancellationToken).ConfigureAwait(false); - callback.AddResult(item); - cancellationToken.ThrowIfCancellationRequested(); - } - } - } -} diff --git a/src/VisualStudio/Core/Def/CallHierarchy/Finders/InterfaceImplementationCallFinder.cs b/src/VisualStudio/Core/Def/CallHierarchy/Finders/InterfaceImplementationCallFinder.cs deleted file mode 100644 index 98bb27558252..000000000000 --- a/src/VisualStudio/Core/Def/CallHierarchy/Finders/InterfaceImplementationCallFinder.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.VisualStudio.Language.CallHierarchy; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; - -internal sealed class InterfaceImplementationCallFinder : AbstractCallFinder -{ - private readonly string _text; - - public InterfaceImplementationCallFinder(ISymbol symbol, ProjectId projectId, IAsynchronousOperationListener asyncListener, CallHierarchyProvider provider) - : base(symbol, projectId, asyncListener, provider) - { - _text = string.Format(EditorFeaturesResources.Calls_To_Interface_Implementation_0, symbol.ToDisplayString()); - } - - public override string DisplayName => _text; - - public override string SearchCategory => CallHierarchyPredefinedSearchCategoryNames.InterfaceImplementations; - - protected override async Task> GetCallersAsync(ISymbol symbol, Project project, IImmutableSet documents, CancellationToken cancellationToken) - { - var calls = await SymbolFinder.FindCallersAsync(symbol, project.Solution, documents, cancellationToken).ConfigureAwait(false); - return calls.Where(c => c.IsDirect); - } -} diff --git a/src/VisualStudio/Core/Def/CallHierarchy/Finders/MethodCallFinder.cs b/src/VisualStudio/Core/Def/CallHierarchy/Finders/MethodCallFinder.cs deleted file mode 100644 index 2445325e63e3..000000000000 --- a/src/VisualStudio/Core/Def/CallHierarchy/Finders/MethodCallFinder.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.VisualStudio.Language.CallHierarchy; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; - -internal sealed class MethodCallFinder : AbstractCallFinder -{ - public MethodCallFinder(ISymbol symbol, ProjectId projectId, IAsynchronousOperationListener asyncListener, CallHierarchyProvider provider) - : base(symbol, projectId, asyncListener, provider) - { - } - - public override string DisplayName - { - get - { - return string.Format(EditorFeaturesResources.Calls_To_0, SymbolName); - } - } - - public override string SearchCategory => CallHierarchyPredefinedSearchCategoryNames.Callers; - - protected override async Task> GetCallersAsync(ISymbol symbol, Project project, IImmutableSet documents, CancellationToken cancellationToken) - { - var callers = await SymbolFinder.FindCallersAsync(symbol, project.Solution, documents, cancellationToken).ConfigureAwait(false); - return callers.Where(c => c.IsDirect); - } -} diff --git a/src/VisualStudio/Core/Def/CallHierarchy/Finders/OverridingMemberFinder.cs b/src/VisualStudio/Core/Def/CallHierarchy/Finders/OverridingMemberFinder.cs deleted file mode 100644 index 553f3c21b030..000000000000 --- a/src/VisualStudio/Core/Def/CallHierarchy/Finders/OverridingMemberFinder.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.Shared.TestHooks; -using Microsoft.VisualStudio.Language.CallHierarchy; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.CallHierarchy.Finders; - -internal sealed class OverridingMemberFinder : AbstractCallFinder -{ - public OverridingMemberFinder(ISymbol symbol, ProjectId projectId, IAsynchronousOperationListener asyncListener, CallHierarchyProvider provider) - : base(symbol, projectId, asyncListener, provider) - { - } - - public override string DisplayName => EditorFeaturesResources.Overrides_; - - public override string SearchCategory => CallHierarchyPredefinedSearchCategoryNames.Overrides; - - protected override Task> GetCallersAsync(ISymbol symbol, Project project, IImmutableSet documents, CancellationToken cancellationToken) - => throw new NotImplementedException(); - - protected override async Task SearchWorkerAsync(ISymbol symbol, Project project, ICallHierarchySearchCallback callback, IImmutableSet documents, CancellationToken cancellationToken) - { - var overrides = await SymbolFinder.FindOverridesAsync(symbol, project.Solution, cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var @override in overrides) - { - var sourceLocations = @override.DeclaringSyntaxReferences.Select(d => project.Solution.GetDocument(d.SyntaxTree)).WhereNotNull(); - var bestLocation = sourceLocations.FirstOrDefault(d => documents == null || documents.Contains(d)); - if (bestLocation != null) - { - var item = await Provider.CreateItemAsync(@override, bestLocation.Project, [], cancellationToken).ConfigureAwait(false); - callback.AddResult(item); - cancellationToken.ThrowIfCancellationRequested(); - } - } - } -} diff --git a/src/VisualStudio/Core/Def/Commands.vsct b/src/VisualStudio/Core/Def/Commands.vsct index 3c33167ba3e6..b5c8c764c7c3 100644 --- a/src/VisualStudio/Core/Def/Commands.vsct +++ b/src/VisualStudio/Core/Def/Commands.vsct @@ -512,6 +512,18 @@ + + + + @@ -807,6 +830,7 @@ + @@ -820,6 +844,8 @@ + + @@ -848,5 +874,10 @@ + + + + + diff --git a/src/VisualStudio/Core/Def/Diagnostics/IVisualStudioDiagnosticAnalyzerService.cs b/src/VisualStudio/Core/Def/Diagnostics/IVisualStudioDiagnosticAnalyzerService.cs index bf59c6afe2a3..06219d84910f 100644 --- a/src/VisualStudio/Core/Def/Diagnostics/IVisualStudioDiagnosticAnalyzerService.cs +++ b/src/VisualStudio/Core/Def/Diagnostics/IVisualStudioDiagnosticAnalyzerService.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; namespace Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics; @@ -32,9 +31,4 @@ internal interface IVisualStudioDiagnosticAnalyzerService /// Otherwise, analyzers are run on the current solution. /// void RunAnalyzers(IVsHierarchy? hierarchy); - - /// - /// Initializes the service. - /// - Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken); } diff --git a/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerService.cs b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerService.cs index 08839c96cecc..a9d971b2cb07 100644 --- a/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerService.cs +++ b/src/VisualStudio/Core/Def/Diagnostics/VisualStudioDiagnosticAnalyzerService.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.Composition; -using System.ComponentModel.Design; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -22,13 +21,13 @@ using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; -using Microsoft.VisualStudio.Threading; using Roslyn.Utilities; using Task = System.Threading.Tasks.Task; namespace Microsoft.VisualStudio.LanguageServices.Implementation.Diagnostics; [Export(typeof(IVisualStudioDiagnosticAnalyzerService))] +[Export(typeof(VisualStudioDiagnosticAnalyzerService))] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed partial class VisualStudioDiagnosticAnalyzerService( @@ -36,11 +35,12 @@ internal sealed partial class VisualStudioDiagnosticAnalyzerService( IVsService statusbar, IThreadingContext threadingContext, IVsHierarchyItemManager vsHierarchyItemManager, - IAsynchronousOperationListenerProvider listenerProvider) : IVisualStudioDiagnosticAnalyzerService + IAsynchronousOperationListenerProvider listenerProvider, + [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) : IVisualStudioDiagnosticAnalyzerService { // "Run Code Analysis on <%ProjectName%>" command for Top level "Build" and "Analyze" menus. // The below ID is actually defined as "ECMD_RUNFXCOPSEL" in stdidcmd.h, we're just referencing it here. - private const int RunCodeAnalysisForSelectedProjectCommandId = 1647; + internal const int RunCodeAnalysisForSelectedProjectCommandId = 1647; private readonly VisualStudioWorkspace _workspace = workspace; private readonly IVsService _statusbar = statusbar; @@ -51,21 +51,7 @@ internal sealed partial class VisualStudioDiagnosticAnalyzerService( private readonly CancellationSeries _cancellationSeries = new(threadingContext.DisposalToken); - private IServiceProvider? _serviceProvider; - - public async Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken) - { - _serviceProvider = (IServiceProvider)serviceProvider; - - // Hook up the "Run Code Analysis" menu command for CPS based managed projects. - var menuCommandService = await serviceProvider.GetServiceAsync(throwOnFailure: false, cancellationToken).ConfigureAwait(false); - if (menuCommandService != null) - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, RunCodeAnalysisForSelectedProjectCommandId, VSConstants.VSStd2K, OnRunCodeAnalysisForSelectedProject, OnRunCodeAnalysisForSelectedProjectStatus); - VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.RunCodeAnalysisForProject, Guids.RoslynGroupId, OnRunCodeAnalysisForSelectedProject, OnRunCodeAnalysisForSelectedProjectStatus); - } - } + private readonly IServiceProvider _serviceProvider = serviceProvider; public async Task>> GetAllDiagnosticDescriptorsAsync( IVsHierarchy? hierarchy, @@ -127,7 +113,7 @@ private static IReadOnlyDictionary> Tr return map.ToDictionary(kv => kv.Key, kv => (IEnumerable)kv.Value); } - private void OnRunCodeAnalysisForSelectedProjectStatus(object sender, EventArgs e) + internal void OnRunCodeAnalysisForSelectedProjectStatus(object sender, EventArgs e) { var command = (OleMenuCommand)sender; @@ -161,7 +147,7 @@ private void OnRunCodeAnalysisForSelectedProjectStatus(object sender, EventArgs } } - private void OnRunCodeAnalysisForSelectedProject(object sender, EventArgs args) + internal void OnRunCodeAnalysisForSelectedProject(object sender, EventArgs args) { if (VisualStudioCommandHandlerHelpers.TryGetSelectedProjectHierarchy(_serviceProvider, out var hierarchy)) RunAnalyzers(hierarchy); diff --git a/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs b/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs index e55fb348cacf..c88b5fc9b38c 100644 --- a/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs +++ b/src/VisualStudio/Core/Def/DocumentOutline/DocumentOutlineViewModel_Utilities.cs @@ -140,7 +140,8 @@ public static ImmutableArray SearchDocumentSymbolData( cancellationToken.ThrowIfCancellationRequested(); using var _ = ArrayBuilder.GetInstance(out var filteredDocumentSymbols); - var patternMatcher = PatternMatcher.CreatePatternMatcher(pattern, includeMatchedSpans: false, allowFuzzyMatching: true); + using var patternMatcher = PatternMatcher.CreatePatternMatcher( + pattern, includeMatchedSpans: false, PatternMatcherKind.Standard | PatternMatcherKind.Fuzzy); foreach (var documentSymbol in documentSymbolData) { @@ -151,7 +152,6 @@ public static ImmutableArray SearchDocumentSymbolData( return filteredDocumentSymbols.ToImmutableAndClear(); - // Returns true if the name of one of the tree nodes results in a pattern match. static bool SearchNodeTree(DocumentSymbolData tree, PatternMatcher patternMatcher, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/VisualStudio/Core/Def/EditAndContinue/EditAndContinueFeedbackDiagnosticFileProvider.cs b/src/VisualStudio/Core/Def/EditAndContinue/EditAndContinueFeedbackDiagnosticFileProvider.cs index 715cbd996bc6..dd47de1c6e8b 100644 --- a/src/VisualStudio/Core/Def/EditAndContinue/EditAndContinueFeedbackDiagnosticFileProvider.cs +++ b/src/VisualStudio/Core/Def/EditAndContinue/EditAndContinueFeedbackDiagnosticFileProvider.cs @@ -12,6 +12,7 @@ using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.Internal.VisualStudio.Shell.Embeddable.Feedback; using Newtonsoft.Json.Linq; @@ -42,22 +43,18 @@ internal sealed class EditAndContinueFeedbackDiagnosticFileProvider : IFeedbackD private readonly int _vsProcessId; private readonly DateTime _vsProcessStartTime; private readonly string _tempDir; - + private readonly Lazy _workspaceProvider; private volatile int _isLogCollectionInProgress; - private readonly Lazy? _encService; - [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public EditAndContinueFeedbackDiagnosticFileProvider( - [Import(AllowDefault = true)] Lazy? encService = null) + public EditAndContinueFeedbackDiagnosticFileProvider(Lazy workspaceProvider) { - _encService = encService; - var vsProcess = Process.GetCurrentProcess(); _vsProcessId = vsProcess.Id; _vsProcessStartTime = vsProcess.StartTime; + _workspaceProvider = workspaceProvider; _tempDir = Path.GetTempPath(); var vsFeedbackTempDir = Path.Combine(_tempDir, VSFeedbackSemaphoreDir); @@ -97,9 +94,23 @@ private string GetZipFilePath() => Path.Combine(Path.Combine(_tempDir, $"EnC_{_vsProcessId}", ZipFileName)); public IReadOnlyCollection GetFiles() - => _vsFeedbackSemaphoreFileWatcher is null - ? [] - : (IReadOnlyCollection)([GetZipFilePath()]); + => _vsFeedbackSemaphoreFileWatcher is null ? [] : [GetZipFilePath()]; + + private void SetFileLoggingDirectory(string? logDirectory) + { + _ = Task.Run(async () => + { + try + { + var proxy = new RemoteEditAndContinueServiceProxy(_workspaceProvider.Value.Workspace.Services.SolutionServices); + await proxy.SetFileLoggingDirectoryAsync(logDirectory, CancellationToken.None).ConfigureAwait(false); + } + catch + { + // ignore + } + }); + } private void OnFeedbackSemaphoreCreatedOrChanged() { @@ -111,7 +122,7 @@ private void OnFeedbackSemaphoreCreatedOrChanged() if (Interlocked.CompareExchange(ref _isLogCollectionInProgress, 1, 0) == 0) { - _encService?.Value.SetFileLoggingDirectory(GetLogDirectory()); + SetFileLoggingDirectory(GetLogDirectory()); } } @@ -119,7 +130,7 @@ private void OnFeedbackSemaphoreDeleted() { if (Interlocked.Exchange(ref _isLogCollectionInProgress, 0) == 1) { - _encService?.Value.SetFileLoggingDirectory(logDirectory: null); + SetFileLoggingDirectory(logDirectory: null); // Including the zip files in VS Feedback is currently on best effort basis. // See https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1714439 diff --git a/src/VisualStudio/Core/Def/IAnalyzerNodeSetup.cs b/src/VisualStudio/Core/Def/IAnalyzerNodeSetup.cs index 8d255e214a5e..1b0e7dab4f6a 100644 --- a/src/VisualStudio/Core/Def/IAnalyzerNodeSetup.cs +++ b/src/VisualStudio/Core/Def/IAnalyzerNodeSetup.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; using Microsoft.VisualStudio.Shell; namespace Microsoft.VisualStudio.LanguageServices; @@ -13,6 +14,6 @@ namespace Microsoft.VisualStudio.LanguageServices; /// internal interface IAnalyzerNodeSetup { - Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken); + Task InitializeAsync(IAsyncServiceProvider serviceProvider, ThreadSafeMenuCommandService menuCommandService, CancellationToken cancellationToken); void Unregister(); } diff --git a/src/VisualStudio/Core/Def/ID.RoslynCommands.cs b/src/VisualStudio/Core/Def/ID.RoslynCommands.cs index dda67b372c74..0c13141a817b 100644 --- a/src/VisualStudio/Core/Def/ID.RoslynCommands.cs +++ b/src/VisualStudio/Core/Def/ID.RoslynCommands.cs @@ -37,6 +37,7 @@ public static class RoslynCommands public const int RemoveUnusedReferences = 0x0202; public const int GoToValueTrackingWindow = 0x0203; public const int SyncNamespaces = 0x0204; + public const int ShowInheritanceMargin = 0x0205; // Document Outline public const int DocumentOutlineToolbar = 0x300; @@ -51,5 +52,7 @@ public static class RoslynCommands public const int SolutionExplorerSymbolItemGoToBase = 0x402; public const int SolutionExplorerSymbolItemGoToImplementation = 0x403; public const int SolutionExplorerSymbolItemFindAllReferences = 0x404; + + public const int LogRoslynWorkspaceStructure = 0x500; } } diff --git a/src/VisualStudio/Core/Def/Implementation/CommandBindings.cs b/src/VisualStudio/Core/Def/Implementation/CommandBindings.cs index 95904553dcee..21b8041abc06 100644 --- a/src/VisualStudio/Core/Def/Implementation/CommandBindings.cs +++ b/src/VisualStudio/Core/Def/Implementation/CommandBindings.cs @@ -33,4 +33,8 @@ internal sealed class CommandBindings [Export] [CommandBinding(Guids.RoslynGroupIdString, ID.RoslynCommands.GoToValueTrackingWindow, typeof(ValueTrackingEditorCommandArgs))] internal CommandBindingDefinition gotoDataFlowToolCommandBinding; + + [Export] + [CommandBinding(Guids.RoslynGroupIdString, ID.RoslynCommands.ShowInheritanceMargin, typeof(ShowInheritanceMarginCommandArgs))] + internal CommandBindingDefinition showInheritanceMarginCommandBinding; } diff --git a/src/VisualStudio/Core/Def/InheritanceMargin/ShowInheritanceMarginCommandHandler.cs b/src/VisualStudio/Core/Def/InheritanceMargin/ShowInheritanceMarginCommandHandler.cs new file mode 100644 index 000000000000..15e77c923c9b --- /dev/null +++ b/src/VisualStudio/Core/Def/InheritanceMargin/ShowInheritanceMarginCommandHandler.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.ComponentModel.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor; +using Microsoft.CodeAnalysis.Editor.Commanding.Commands; +using Microsoft.CodeAnalysis.Editor.Shared.Extensions; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.InheritanceMargin; +using Microsoft.CodeAnalysis.Options; +using Microsoft.VisualStudio.Commanding; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.InheritanceMargin; + +[Export(typeof(ICommandHandler))] +[ContentType(ContentTypeNames.CSharpContentType)] +[ContentType(ContentTypeNames.VisualBasicContentType)] +[Name(nameof(ShowInheritanceMarginCommandHandler))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ShowInheritanceMarginCommandHandler(IGlobalOptionService globalOptions) : ICommandHandler +{ + public string DisplayName => ServicesVSResources.Show_Inheritance; + + public CommandState GetCommandState(ShowInheritanceMarginCommandArgs args) + { + var language = args.SubjectBuffer.GetLanguageName(); + if (language is null) + { + return CommandState.Unspecified; + } + + var isChecked = globalOptions.GetOption(InheritanceMarginOptionsStorage.ShowInheritanceMargin, language) ?? true; + return new CommandState(isAvailable: true, isChecked); + } + + public bool ExecuteCommand(ShowInheritanceMarginCommandArgs args, CommandExecutionContext context) + { + var language = args.SubjectBuffer.GetLanguageName(); + if (language is null) + { + return false; + } + + var current = globalOptions.GetOption(InheritanceMarginOptionsStorage.ShowInheritanceMargin, language) ?? true; + globalOptions.SetGlobalOption(InheritanceMarginOptionsStorage.ShowInheritanceMargin, language, !current); + return true; + } +} diff --git a/src/VisualStudio/Core/Def/Library/ObjectBrowser/AbstractObjectBrowserLibraryManager.cs b/src/VisualStudio/Core/Def/Library/ObjectBrowser/AbstractObjectBrowserLibraryManager.cs index 5d543edc697b..b99e71ff9222 100644 --- a/src/VisualStudio/Core/Def/Library/ObjectBrowser/AbstractObjectBrowserLibraryManager.cs +++ b/src/VisualStudio/Core/Def/Library/ObjectBrowser/AbstractObjectBrowserLibraryManager.cs @@ -29,7 +29,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.Library.ObjectB internal abstract partial class AbstractObjectBrowserLibraryManager : AbstractLibraryManager, IDisposable { - internal readonly VisualStudioWorkspace Workspace; + private readonly Lazy _workspace; + + internal VisualStudioWorkspace Workspace => _workspace.Value; internal ILibraryService LibraryService => _libraryService.Value; @@ -47,14 +49,21 @@ protected AbstractObjectBrowserLibraryManager( string languageName, Guid libraryGuid, IServiceProvider serviceProvider, - IComponentModel componentModel, - VisualStudioWorkspace workspace) + IComponentModel componentModel) : base(libraryGuid, componentModel, serviceProvider) { _languageName = languageName; - Workspace = workspace; - _workspaceChangedDisposer = Workspace.RegisterWorkspaceChangedHandler(OnWorkspaceChanged); + _workspace = new Lazy(() => + { + var workspace = ComponentModel.GetService(); + + // We will now register for WorkspaceChanged now. Since that's used to invalidate previously cached results, there was no reason to be subscribed earlier + // since nothing could have observed the workspace. OnWorkspaceChanged could run during the rest of this Lazy -- in that case it'll just wait + // for the Lazy to complete. If anything else is put after RegisterWorkspaceChangedHandler, think carefully about the potential ordering. + _workspaceChangedDisposer = workspace.RegisterWorkspaceChangedHandler(OnWorkspaceChanged); + return workspace; + }); _libraryService = new Lazy(() => Workspace.Services.GetLanguageServices(_languageName).GetService()); } @@ -519,7 +528,7 @@ private static async Task FindReferencesAsync( try { - // Switch to teh background so we don't block the calling thread (the UI thread) while we're doing this work. + // Switch to the background so we don't block the calling thread (the UI thread) while we're doing this work. await TaskScheduler.Default; await FindReferencesAsync(symbolListItem, project, context, classificationOptions, cancellationToken).ConfigureAwait(false); } diff --git a/src/VisualStudio/Core/Def/Microsoft.VisualStudio.LanguageServices.csproj b/src/VisualStudio/Core/Def/Microsoft.VisualStudio.LanguageServices.csproj index 789456f3827d..dc06773ef93b 100644 --- a/src/VisualStudio/Core/Def/Microsoft.VisualStudio.LanguageServices.csproj +++ b/src/VisualStudio/Core/Def/Microsoft.VisualStudio.LanguageServices.csproj @@ -43,6 +43,7 @@ Shared\ConcurrentLruCache.cs + True True @@ -102,7 +103,7 @@ - + diff --git a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs index 8049802a1428..d37f20cfc444 100644 --- a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs +++ b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs @@ -394,6 +394,7 @@ public bool TryFetch(LocalUserRegistryOptionPersister persister, OptionKey2 opti {"dotnet_enable_snippets", new LocalUserProfileStorage(@"Roslyn\Internal\OnOff\Features", "Snippets2")}, {"dotnet_enable_syntactic_colorizer", new LocalUserProfileStorage(@"Roslyn\Internal\OnOff\Features", "Syntactic Colorizer")}, {"dotnet_enable_solution_crawler", new LocalUserProfileStorage(@"Roslyn\Internal\SolutionCrawler", "Solution Crawler")}, + {"dotnet_process_roslyn_source_generated_files_in_solution_crawler", new UnifiedSettingsManagerStorage("test.includeSourceGeneratedFilesInRealTimeDiscovery")}, {"dotnet_colorize_json_patterns", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.ColorizeJsonPatterns")}, {"dotnet_unsupported_detect_and_offer_editor_features_for_probable_json_strings", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.DetectAndOfferEditorFeaturesForProbableJsonStrings")}, {"dotnet_highlight_related_json_components", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.HighlightRelatedJsonComponentsUnderCursor")}, diff --git a/src/VisualStudio/Core/Def/Options/VisualStudioUnifiedSettingsOptionPersister.cs b/src/VisualStudio/Core/Def/Options/VisualStudioUnifiedSettingsOptionPersister.cs index 78ee53c51950..fd8f2e1dbb20 100644 --- a/src/VisualStudio/Core/Def/Options/VisualStudioUnifiedSettingsOptionPersister.cs +++ b/src/VisualStudio/Core/Def/Options/VisualStudioUnifiedSettingsOptionPersister.cs @@ -31,7 +31,9 @@ private void OnSettingChanged(SettingsUpdate update) private static void CheckStorageKeyAndType(string storageKey, [NotNull] Type? storageType) { - Contract.ThrowIfFalse(storageKey.StartsWith("languages"), "Need to update SubscribeToChanges in constructor to listen to changes to this key"); + // HACK: We don't need to listen to changes to test.includeSourceGeneratedFilesInRealTimeDiscovery because we require a restart anyways, so just ignore it rather than + // worrying that we're watching it. + Contract.ThrowIfFalse(storageKey.StartsWith("languages") || storageKey == "test.includeSourceGeneratedFilesInRealTimeDiscovery", "Need to update SubscribeToChanges in constructor to listen to changes to this key"); // Currently, these are the only types we expect. This can be augmented in the future if we need to serialize // more kinds to unified settings backend. diff --git a/src/VisualStudio/Core/Def/PackageRegistration.pkgdef b/src/VisualStudio/Core/Def/PackageRegistration.pkgdef index 13038e3ef47e..132e7ceabd5b 100644 --- a/src/VisualStudio/Core/Def/PackageRegistration.pkgdef +++ b/src/VisualStudio/Core/Def/PackageRegistration.pkgdef @@ -82,3 +82,10 @@ "IsAsyncQueryable"=dword:00000001 "IsCacheable"=dword:00000001 "IsFreeThreaded"=dword:00000001 + +// CacheTag value should be changed when registration file changes +// See https://devdiv.visualstudio.com/DevDiv/_wiki/wikis/DevDiv.wiki/39345/Manifest-Build-Deployment-and-Setup-Authoring-In-Depth?anchor=example-pkgdef-key for more infomation +[$RootKey$\SettingsManifests\{6cf2e545-6109-4730-8883-cf43d7aec3e1}] +@="Microsoft.VisualStudio.LanguageServices.Setup.RoslynPackage" +"ManifestPath"="$PackageFolder$\UnifiedSettings\roslynSettings.registration.json" +"CacheTag"=qword:ABE9BBF01CC8B289 diff --git a/src/VisualStudio/Core/Def/Preview/FileChange.cs b/src/VisualStudio/Core/Def/Preview/FileChange.cs index 5fb9516fd0d8..4b8f18b59d18 100644 --- a/src/VisualStudio/Core/Def/Preview/FileChange.cs +++ b/src/VisualStudio/Core/Def/Preview/FileChange.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.Threading; @@ -25,8 +23,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.Preview; internal sealed class FileChange : AbstractChange { - private readonly TextDocument _left; - private readonly TextDocument _right; + private readonly TextDocument? _left; + private readonly TextDocument? _right; private readonly IComponentModel _componentModel; public readonly DocumentId Id; private readonly ITextBuffer _buffer; @@ -37,8 +35,8 @@ internal sealed class FileChange : AbstractChange DifferenceType = StringDifferenceTypes.Line, }; - public FileChange(TextDocument left, - TextDocument right, + public FileChange(TextDocument? left, + TextDocument? right, IComponentModel componentModel, AbstractChange parent, PreviewEngine engine, @@ -46,7 +44,7 @@ public FileChange(TextDocument left, { Contract.ThrowIfFalse(left != null || right != null); - this.Id = left != null ? left.Id : right.Id; + this.Id = left != null ? left.Id : right!.Id; _left = left; _right = right; _imageService = imageService; @@ -56,7 +54,7 @@ public FileChange(TextDocument left, var bufferCloneService = componentModel.GetService(); var bufferText = left != null ? left.GetTextSynchronously(CancellationToken.None) - : right.GetTextSynchronously(CancellationToken.None); + : right!.GetTextSynchronously(CancellationToken.None); _buffer = bufferCloneService.Clone(bufferText, bufferFactory.InertContentType); @@ -64,12 +62,12 @@ public FileChange(TextDocument left, this.parent = parent; } - private ChangeList ComputeChildren(TextDocument left, TextDocument right, CancellationToken cancellationToken) + private ChangeList ComputeChildren(TextDocument? left, TextDocument? right, CancellationToken cancellationToken) { if (left == null) { // Added document. - return GetEntireDocumentAsSpanChange(right); + return GetEntireDocumentAsSpanChange(right!); } else if (right == null) { @@ -81,12 +79,13 @@ private ChangeList ComputeChildren(TextDocument left, TextDocument right, Cancel var newText = right.GetTextSynchronously(cancellationToken); var diffSelector = _componentModel.GetService(); + var bufferFactoryService = _componentModel.GetService(); var diffService = diffSelector.GetTextDifferencingService( - left.Project.Services.GetService().GetDefaultContentType()); + left.Project.Services.GetRequiredService().GetDefaultContentType()); diffService ??= diffSelector.DefaultTextDifferencingService; - var diff = ComputeDiffSpans(diffService, left, right, cancellationToken); + var diff = ComputeDiffSpans(diffService, left, right, bufferFactoryService, cancellationToken); if (diff.Differences.Count == 0) { // There are no changes. @@ -148,7 +147,7 @@ public override int GetText(out VSTREETEXTOPTIONS tto, out string pbstrText) { if (_left == null) { - pbstrText = ServicesVSResources.bracket_plus_bracket + _right.Name; + pbstrText = ServicesVSResources.bracket_plus_bracket + _right!.Name; } else if (_right == null) { @@ -163,7 +162,7 @@ public override int GetText(out VSTREETEXTOPTIONS tto, out string pbstrText) return VSConstants.S_OK; } - public override int GetTipText(out VSTREETOOLTIPTYPE eTipType, out string pbstrText) + public override int GetTipText(out VSTREETOOLTIPTYPE eTipType, out string? pbstrText) { eTipType = VSTREETOOLTIPTYPE.TIPTYPE_DEFAULT; pbstrText = null; @@ -196,10 +195,10 @@ private SourceText UpdateBufferText() return _buffer.CurrentSnapshot.AsText(); } - public TextDocument GetOldDocument() + public TextDocument? GetOldDocument() => _left; - public TextDocument GetUpdatedDocument() + public TextDocument? GetUpdatedDocument() { if (_left == null || _right == null) { @@ -211,11 +210,11 @@ public TextDocument GetUpdatedDocument() } // Note that either _left or _right *must* be non-null (we are either adding, removing or changing a file). - public TextDocumentKind ChangedDocumentKind => (_left ?? _right).Kind; + public TextDocumentKind ChangedDocumentKind => (_left ?? _right!).Kind; internal override void GetDisplayData(VSTREEDISPLAYDATA[] pData) { - var document = _right ?? _left; + var document = _right ?? _left!; // If these are documents from a VS workspace, then attempt to get the right display // data from the underlying VSHierarchy and itemids for the document. @@ -234,7 +233,7 @@ internal override void GetDisplayData(VSTREEDISPLAYDATA[] pData) (ushort)StandardGlyphGroup.GlyphGroupClass; } - private static IHierarchicalDifferenceCollection ComputeDiffSpans(ITextDifferencingService diffService, TextDocument left, TextDocument right, CancellationToken cancellationToken) + private static IHierarchicalDifferenceCollection ComputeDiffSpans(ITextDifferencingService diffService, TextDocument left, TextDocument right, ITextBufferFactoryService bufferFactoryService, CancellationToken cancellationToken) { // TODO: it would be nice to have a syntax based differ for presentation here, // current way of just using text differ has its own issue, and using syntax differ in compiler that are for incremental parser @@ -243,6 +242,6 @@ private static IHierarchicalDifferenceCollection ComputeDiffSpans(ITextDifferenc var oldText = left.GetTextSynchronously(cancellationToken); var newText = right.GetTextSynchronously(cancellationToken); - return diffService.DiffSourceTexts(oldText, newText, s_differenceOptions); + return diffService.DiffSourceTexts(oldText, newText, bufferFactoryService, s_differenceOptions); } } diff --git a/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs index c87c7e386157..2c97d2a2b17b 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/BrokeredService/WorkspaceProject.cs @@ -25,7 +25,12 @@ public WorkspaceProject(IWorkspaceProjectContext project) public void Dispose() { + // Because this implementation needs to implement the contract for RpcMarshalable + // we have to implement IDisposable, so we don't have an alternative to implement + // at this point. +#pragma warning disable CS0618 // Type or member is obsolete _project.Dispose(); +#pragma warning restore CS0618 // Type or member is obsolete } [Obsolete($"Call the {nameof(AddAdditionalFilesAsync)} overload that takes {nameof(SourceFileInfo)}.")] diff --git a/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs b/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs index 621188726651..30f70afd8686 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/CPS/IWorkspaceProjectContext.cs @@ -15,10 +15,14 @@ namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem; /// Project context to initialize properties and items of a Workspace project created by . /// /// -/// is safe to call on instances of this type on any thread. +/// All methods are safe to call from any thread. /// -internal interface IWorkspaceProjectContext : IDisposable +internal interface IWorkspaceProjectContext : IDisposable, IAsyncDisposable { + /// + [Obsolete("Use DisposeAsync instead.")] + new void Dispose(); + // Project properties. string DisplayName { get; set; } string? ProjectFilePath { get; set; } diff --git a/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject.cs b/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject.cs index a9a19c1665b4..ef9d2ce753a5 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/Legacy/AbstractLegacyProject.cs @@ -19,6 +19,7 @@ using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList; using Microsoft.VisualStudio.LanguageServices.ProjectSystem; using Microsoft.VisualStudio.LanguageServices.ProjectSystem.Legacy; +using Microsoft.VisualStudio.LanguageServices.TaskList; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Roslyn.Utilities; @@ -148,6 +149,9 @@ public AbstractLegacyProject( var projectHierarchyGuid = GetProjectIDGuid(hierarchy); + var diagnosticCache = workspaceImpl.Services.GetRequiredService(); + diagnosticCache.RegisterProject(ProjectSystemProject.Id); + _externalErrorReporter = new ProjectExternalErrorReporter(ProjectSystemProject.Id, projectHierarchyGuid, externalErrorReportingPrefix, language, workspaceImpl); _batchScopeCreator = componentModel.GetService(); _batchScopeCreator.StartTrackingProject(ProjectSystemProject, Hierarchy); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/Logging/RoslynWorkspaceStructureLogger.cs b/src/VisualStudio/Core/Def/ProjectSystem/Logging/RoslynWorkspaceStructureLogger.cs new file mode 100644 index 000000000000..791333e75780 --- /dev/null +++ b/src/VisualStudio/Core/Def/ProjectSystem/Logging/RoslynWorkspaceStructureLogger.cs @@ -0,0 +1,375 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Shell.FileDialog; +using System.Xml.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host; +using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Extensions; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Threading; + +#pragma warning disable CA2007 // We are OK awaiting tasks since we're following Visual Studio threading rules in this file + +namespace Microsoft.VisualStudio.LanguageServices.ProjectSystem.Logging; + +internal static class RoslynWorkspaceStructureLogger +{ + private static int s_NextCompilationId; + private static readonly ConditionalWeakTable> s_CompilationIds = new(); + + public static void ShowSaveDialogAndLog(IServiceProvider serviceProvider) + { + ThreadHelper.ThrowIfNotOnUIThread(); + + var uiShell = (IVsUIShell)serviceProvider.GetService(typeof(SVsUIShell)); + var uiShell2 = (IVsUIShell2)uiShell; + + Assumes.Present(uiShell2); + + ErrorHandler.ThrowOnFailure(uiShell.GetDialogOwnerHwnd(out var hwnd)); + + var filters = new DialogFilters( + new[] { new DialogFilter("Zip Files", "*.zip") }, + defaultFilterIndex: 0); + + var path = VsShellUtilities.SelectSaveAsFile( + uiShell2, + hwnd, + title: string.Empty, + initialDirectory: string.Empty, + initialFileName: string.Empty, + filters); + + if (string.IsNullOrEmpty(path)) + return; + + var threadingContext = serviceProvider.GetMefService(); + + threadingContext.JoinableTaskFactory.RunAsync(() => LogAsync(serviceProvider, threadingContext, path)); + } + + public static async Task LogAsync(IServiceProvider serviceProvider, IThreadingContext threadingContext, string path) + { + var componentModel = (IComponentModel)serviceProvider.GetService(typeof(SComponentModel)); + Assumes.Present(componentModel); + + var workspace = componentModel.GetService(); + var solution = workspace.CurrentSolution; + + // Start a threaded wait dialog + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(); + var dialogFactory = (IVsThreadedWaitDialogFactory)serviceProvider.GetService(typeof(SVsThreadedWaitDialogFactory)); + Assumes.Present(dialogFactory); + using var session = dialogFactory.StartWaitDialog( + ServicesVSResources.Visual_Studio, + new ThreadedWaitDialogProgressData( + ServicesVSResources.Logging_Roslyn_Workspace_structure, + progressText: null, + statusBarText: null, + isCancelable: true, + currentStep: 0, + totalSteps: solution.ProjectIds.Count), + delayToShowDialog: TimeSpan.Zero); + var cancellationToken = session.UserCancellationToken; + + // Now switch to the background thread while we're working + await TaskScheduler.Default; + + try + { + var document = new XDocument(); + var workspaceElement = new XElement("workspace"); + workspaceElement.SetAttributeValue("kind", workspace.Kind); + document.Add(workspaceElement); + + var projectsProcessed = 0; + + foreach (var project in solution.GetProjectDependencyGraph().GetTopologicallySortedProjects(cancellationToken).Select(solution.GetProject)) + { + if (project is null) + continue; + + // Dump basic project attributes + var projectElement = new XElement("project"); + workspaceElement.Add(projectElement); + + projectElement.SetAttributeValue("id", SanitizePath(project.Id.ToString())); + projectElement.SetAttributeValue("name", project.Name); + projectElement.SetAttributeValue("assemblyName", project.AssemblyName); + projectElement.SetAttributeValue("language", project.Language); + projectElement.SetAttributeValue("path", SanitizePath(project.FilePath ?? "(none)")); + projectElement.SetAttributeValue("outputPath", SanitizePath(project.OutputFilePath ?? "(none)")); + + var hasSuccessfullyLoaded = await project.HasSuccessfullyLoadedAsync(cancellationToken); + projectElement.SetAttributeValue("hasSuccessfullyLoaded", hasSuccessfullyLoaded); + + // Dump MSBuild nodes + var msbuildReferencesElement = CreateMsBuildReferencesElement(project); + if (msbuildReferencesElement != null) + projectElement.Add(msbuildReferencesElement); + + // Dump DTE references + var dteReferencesElement = await CreateDteReferencesElementAsync(serviceProvider, threadingContext, project); + if (dteReferencesElement != null) + projectElement.Add(dteReferencesElement); + + // Dump the actual metadata references in the workspace + var workspaceReferencesElement = new XElement("workspaceReferences"); + projectElement.Add(workspaceReferencesElement); + + foreach (var metadataReference in project.MetadataReferences) + { + workspaceReferencesElement.Add(CreateElementForPortableExecutableReference(metadataReference)); + } + + // Dump project references in the workspace + foreach (var projectReference in project.AllProjectReferences) + { + var referenceElement = new XElement("projectReference", new XAttribute("id", SanitizePath(projectReference.ProjectId.ToString()))); + + if (!project.ProjectReferences.Contains(projectReference)) + referenceElement.SetAttributeValue("missingInSolution", "true"); + + workspaceReferencesElement.Add(referenceElement); + } + + projectElement.Add(new XElement("workspaceDocuments", await CreateElementsForDocumentCollectionAsync(project.Documents, "document", cancellationToken))); + projectElement.Add(new XElement("workspaceAdditionalDocuments", await CreateElementsForDocumentCollectionAsync(project.AdditionalDocuments, "additionalDocuments", cancellationToken))); + + projectElement.Add(new XElement("workspaceAnalyzerConfigDocuments", await CreateElementsForDocumentCollectionAsync(project.AnalyzerConfigDocuments, "analyzerConfigDocument", cancellationToken))); + + // Dump references from the compilation; this should match the workspace but can help rule out + // cross-language reference bugs or other issues like that + var compilation = await project.GetCompilationAsync(cancellationToken); + + if (compilation != null) + { + var compilationReferencesElement = new XElement("compilationReferences"); + projectElement.Add(compilationReferencesElement); + + foreach (var reference in compilation.References) + { + compilationReferencesElement.Add(CreateElementForPortableExecutableReference(reference)); + } + + projectElement.Add(CreateElementForCompilation(compilation)); + + // Dump all diagnostics + var diagnosticsElement = new XElement("diagnostics"); + projectElement.Add(diagnosticsElement); + + foreach (var diagnostic in compilation.GetDiagnostics(cancellationToken)) + { + diagnosticsElement.Add( + new XElement("diagnostic", + new XAttribute("id", diagnostic.Id), + new XAttribute("severity", diagnostic.Severity.ToString()), + new XAttribute("path", SanitizePath(diagnostic.Location.GetLineSpan().Path ?? "(none)")), + diagnostic.GetMessage())); + } + } + + projectsProcessed++; + session.Progress.Report(new ThreadedWaitDialogProgressData( + ServicesVSResources.Logging_Roslyn_Workspace_structure, + progressText: null, + statusBarText: null, + isCancelable: true, + currentStep: projectsProcessed, + totalSteps: solution.ProjectIds.Count)); + } + + File.Delete(path); + + using (var zipFile = ZipFile.Open(path, ZipArchiveMode.Create)) + { + var zipFileEntry = zipFile.CreateEntry("Workspace.xml", CompressionLevel.Fastest); + using (var stream = zipFileEntry.Open()) + { + document.Save(stream); + } + } + } + catch (OperationCanceledException) + { + // They cancelled + } + } + + private static XElement? CreateMsBuildReferencesElement(Project project) + { + if (project.FilePath == null) + return null; + + var msbuildProject = XDocument.Load(project.FilePath); + var msbuildNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003"); + + var msbuildReferencesElement = new XElement("msbuildReferences"); + + msbuildReferencesElement.Add(msbuildProject.Descendants(msbuildNamespace + "ProjectReference")); + msbuildReferencesElement.Add(msbuildProject.Descendants(msbuildNamespace + "Reference")); + msbuildReferencesElement.Add(msbuildProject.Descendants(msbuildNamespace + "ReferencePath")); + + return msbuildReferencesElement; + } + + private static async Task CreateDteReferencesElementAsync(IServiceProvider serviceProvider, IThreadingContext threadingContext, Project project) + { + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(); + + var dte = (EnvDTE.DTE)serviceProvider.GetService(typeof(SDTE)); + + VSLangProj.VSProject? langProjProject = null; + foreach (EnvDTE.Project p in dte.Solution.Projects) + { + try + { + if (string.Equals(p.FullName, project.FilePath, StringComparison.OrdinalIgnoreCase)) + { + langProjProject = p.Object as VSLangProj.VSProject; + if (langProjProject is not null) + break; + } + } + catch (NotImplementedException) + { + // Some EnvDTE.Projects will throw on p.FullName, so just bail in that case. + } + } + + if (langProjProject == null) + return null; + + var dteReferences = new XElement("dteReferences"); + + foreach (var reference in langProjProject.References.Cast()) + { + if (reference.SourceProject != null) + { + dteReferences.Add(new XElement("projectReference", new XAttribute("projectName", reference.SourceProject.Name))); + } + else + { + dteReferences.Add(new XElement("metadataReference", + reference.Path != null ? new XAttribute("path", SanitizePath(reference.Path)) : null, + new XAttribute("name", reference.Name))); + } + } + + return dteReferences; + } + + private static string SanitizePath(string s) + { + return ReplacePathComponent(s, Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "%USERPROFILE%"); + } + + /// + /// Equivalent to string.Replace, but uses OrdinalIgnoreCase for matching. + /// + private static string ReplacePathComponent(string s, string oldValue, string newValue) + { + while (true) + { + var index = s.IndexOf(oldValue, StringComparison.OrdinalIgnoreCase); + if (index == -1) + return s; + + s = s.Substring(0, index) + newValue + s.Substring(index + oldValue.Length); + } + } + + private static XElement CreateElementForPortableExecutableReference(MetadataReference reference) + { + var aliasesAttribute = new XAttribute("aliases", string.Join(",", reference.Properties.Aliases)); + + if (reference is CompilationReference compilationReference) + { + return new XElement("compilationReference", + aliasesAttribute, + CreateElementForCompilation(compilationReference.Compilation)); + } + else if (reference is PortableExecutableReference portableExecutableReference) + { + return new XElement("peReference", + new XAttribute("file", SanitizePath(portableExecutableReference.FilePath ?? "(none)")), + new XAttribute("display", SanitizePath(portableExecutableReference.Display ?? "(none)")), + aliasesAttribute); + } + else + { + return new XElement("metadataReference", new XAttribute("display", SanitizePath(reference.Display ?? "(none)"))); + } + } + + private static XElement CreateElementForCompilation(Compilation compilation) + { + StrongBox compilationId; + if (!s_CompilationIds.TryGetValue(compilation, out compilationId)) + { + compilationId = new StrongBox(s_NextCompilationId++); + s_CompilationIds.Add(compilation, compilationId); + } + + var namespaces = new Queue(); + var typesElement = new XElement("types"); + + namespaces.Enqueue(compilation.Assembly.GlobalNamespace); + + while (namespaces.Count > 0) + { + var @ns = namespaces.Dequeue(); + + foreach (var type in @ns.GetTypeMembers()) + { + typesElement.Add(new XElement("type", new XAttribute("name", type.ToDisplayString()))); + } + + foreach (var childNamespace in @ns.GetNamespaceMembers()) + { + namespaces.Enqueue(childNamespace); + } + } + + return new XElement("compilation", + new XAttribute("objectId", compilationId.Value), + new XAttribute("assemblyIdentity", compilation.Assembly.Identity.ToString()), + typesElement); + } + + public static async Task> CreateElementsForDocumentCollectionAsync(IEnumerable documents, string elementName, CancellationToken cancellationToken) + { + var elements = new List(); + + foreach (var document in documents) + { + var documentElement = new XElement(elementName, new XAttribute("path", SanitizePath(document.FilePath ?? "(none)"))); + + var clientName = document.DocumentServiceProvider.GetService()?.DiagnosticsLspClientName; + if (clientName != null) + documentElement.SetAttributeValue("clientName", clientName); + + var loadDiagnostic = await document.State.GetFailedToLoadExceptionMessageAsync(cancellationToken); + + if (loadDiagnostic != null) + documentElement.Add(new XElement("loadDiagnostic", loadDiagnostic)); + + elements.Add(documentElement); + } + + return elements; + } +} diff --git a/src/VisualStudio/Core/Def/ProjectSystem/MiscellaneousFilesWorkspace.cs b/src/VisualStudio/Core/Def/ProjectSystem/MiscellaneousFilesWorkspace.cs index 7e82dc6a8a2a..35cf09c1c307 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/MiscellaneousFilesWorkspace.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/MiscellaneousFilesWorkspace.cs @@ -298,7 +298,7 @@ private ProjectInfo CreateProjectInfoForDocument(string filePath) var checksumAlgorithm = SourceHashAlgorithms.Default; var fileLoader = new WorkspaceFileTextLoader(Services.SolutionServices, filePath, defaultEncoding: null); return MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( - this, filePath, fileLoader, languageInformation, checksumAlgorithm, Services.SolutionServices, _metadataReferences.Value); + this, filePath, fileLoader, languageInformation, checksumAlgorithm, Services.SolutionServices, _metadataReferences.Value, enableFileBasedPrograms: false); } private void DetachFromDocument(string moniker) diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs index 7b7413b2bc64..7b4e8eeb8daf 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioProjectFactory.cs @@ -83,14 +83,15 @@ public async Task CreateAndAddToWorkspaceAsync( var solution = await _solution.GetValueOrNullAsync(cancellationToken).ConfigureAwait(true); - // From this point on, we start mutating the solution. So make us non cancellable. - cancellationToken = CancellationToken.None; - _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionPath = solution?.SolutionFileName; _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.SolutionTelemetryId = GetSolutionSessionId(); var hostInfo = new ProjectSystemHostInfo(_dynamicFileInfoProviders, _analyzerAssemblyRedirectors); - var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo).ConfigureAwait(true); + var project = await _visualStudioWorkspaceImpl.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync(projectSystemName, language, creationInfo, hostInfo, cancellationToken).ConfigureAwait(true); + + // We have now created the project and added it to the solution -- we are committed at this point + // to returning a project or else we would never have a way to remove this project we created. + cancellationToken = CancellationToken.None; _visualStudioWorkspaceImpl.AddProjectToInternalMaps(project, creationInfo.Hierarchy, creationInfo.ProjectGuid, projectSystemName); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs index 71b867721016..a694a2d3d930 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/VisualStudioWorkspaceImpl.cs @@ -90,6 +90,10 @@ internal abstract partial class VisualStudioWorkspaceImpl : VisualStudioWorkspac /// Should be updated with . private ImmutableDictionary> _projectToRuleSetFilePath = ImmutableDictionary>.Empty; + /// + /// Mapping from project system name to a list of projects. + /// Only access when holding + /// private readonly Dictionary> _projectSystemNameToProjectsMap = []; /// @@ -1559,21 +1563,24 @@ internal override bool CanAddProjectReference(ProjectId referencingProject, Proj internal void RemoveProjectFromMaps(CodeAnalysis.Project project) { - foreach (var (projectName, projects) in _projectSystemNameToProjectsMap) + using (_gate.DisposableWait()) { - if (projects.RemoveAll(p => p.Id == project.Id) > 0) + foreach (var (projectName, projects) in _projectSystemNameToProjectsMap) { - if (projects.Count == 0) + if (projects.RemoveAll(p => p.Id == project.Id) > 0) { - _projectSystemNameToProjectsMap.Remove(projectName); - } + if (projects.Count == 0) + { + _projectSystemNameToProjectsMap.Remove(projectName); + } - break; + break; + } } - } - _projectToHierarchyMap = _projectToHierarchyMap.Remove(project.Id); - _projectToGuidMap = _projectToGuidMap.Remove(project.Id); + _projectToHierarchyMap = _projectToHierarchyMap.Remove(project.Id); + _projectToGuidMap = _projectToGuidMap.Remove(project.Id); + } ImmutableInterlocked.TryRemove(ref _projectToRuleSetFilePath, project.Id, out _); diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs index 9de5ffc01e61..c71629a7148f 100644 --- a/src/VisualStudio/Core/Def/RoslynPackage.cs +++ b/src/VisualStudio/Core/Def/RoslynPackage.cs @@ -21,6 +21,7 @@ using Microsoft.VisualStudio.LanguageServices.Implementation.SyncNamespaces; using Microsoft.VisualStudio.LanguageServices.Implementation.TableDataSource; using Microsoft.VisualStudio.LanguageServices.Implementation.UnusedReferences; +using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; using Microsoft.VisualStudio.LanguageServices.InheritanceMargin; using Microsoft.VisualStudio.LanguageServices.ProjectSystem; using Microsoft.VisualStudio.LanguageServices.ProjectSystem.BrokeredService; @@ -41,6 +42,7 @@ internal sealed class RoslynPackage : AbstractPackage { private static RoslynPackage? s_lazyInstance; + private ThreadSafeMenuCommandService? _menuCommandService; private RuleSetEventHandler? _ruleSetEventHandler; private SolutionEventMonitor? _solutionEventMonitor; @@ -75,6 +77,35 @@ async Task PackageInitializationBackgroundThreadAsync(PackageLoadTasks packageIn { AddService(typeof(RoslynPackageLoadService), (_, _, _) => Task.FromResult((object?)new RoslynPackageLoadService()), promote: true); + var menuCommandService = await this.GetServiceAsync(throwOnFailure: true, cancellationToken).ConfigureAwait(false); + Assumes.Present(menuCommandService); + _menuCommandService = new ThreadSafeMenuCommandService(menuCommandService); + + _menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.RemoveUnusedReferences, + (s, e) => ComponentModel.GetService().OnRemoveUnusedReferencesForSelectedProject(s, e), + (s, e) => ComponentModel.GetService().OnRemoveUnusedReferencesForSelectedProjectStatus(s, e)); + + _menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.SyncNamespaces, + (s, e) => ComponentModel.GetService().OnSyncNamespacesForSelectedProject(s, e), + (s, e) => ComponentModel.GetService().OnSyncNamespacesForSelectedProjectStatus(s, e)); + + _menuCommandService.AddCommand(VSConstants.VSStd2K, VisualStudioDiagnosticAnalyzerService.RunCodeAnalysisForSelectedProjectCommandId, + (s, e) => ComponentModel.GetService().OnRunCodeAnalysisForSelectedProject(s, e), + (s, e) => ComponentModel.GetService().OnRunCodeAnalysisForSelectedProjectStatus(s, e)); + _menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.RunCodeAnalysisForProject, + (s, e) => ComponentModel.GetService().OnRunCodeAnalysisForSelectedProject(s, e), + (s, e) => ComponentModel.GetService().OnRunCodeAnalysisForSelectedProjectStatus(s, e)); + + _menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.LogRoslynWorkspaceStructure, + (_, _) => ProjectSystem.Logging.RoslynWorkspaceStructureLogger.ShowSaveDialogAndLog(this)); + + _menuCommandService.AddCommand(Guids.StackTraceExplorerCommandId, 0x0100, + (s, e) => ComponentModel.GetService().OnExecute(s, e)); + _menuCommandService.AddCommand(Guids.StackTraceExplorerCommandId, 0x0101, + (s, e) => ComponentModel.GetService().OnPaste(s, e)); + _menuCommandService.AddCommand(Guids.StackTraceExplorerCommandId, 0x0102, + (s, e) => ComponentModel.GetService().OnClear(s, e)); + await RegisterEditorFactoryAsync(new SettingsEditorFactory(), cancellationToken).ConfigureAwait(true); await ProfferServiceBrokerServicesAsync(cancellationToken).ConfigureAwait(true); } @@ -87,28 +118,23 @@ private async Task ProfferServiceBrokerServicesAsync(CancellationToken cancellat serviceBrokerContainer.Proffer( WorkspaceProjectFactoryServiceDescriptor.ServiceDescriptor, - (_, _, _, _) => ValueTask.FromResult(new WorkspaceProjectFactoryService(this.ComponentModel.GetService()))); - - // Must be profferred before any C#/VB projects are loaded and the corresponding UI context activated. - serviceBrokerContainer.Proffer( - ManagedHotReloadLanguageServiceDescriptor.Descriptor, - (_, _, _, _) => ValueTask.FromResult(new ManagedEditAndContinueLanguageServiceBridge(this.ComponentModel.GetService()))); + (_, _, _, _) => ValueTask.FromResult(new WorkspaceProjectFactoryService(ComponentModel.GetService()))); } protected override async Task LoadComponentsInBackgroundAfterSolutionFullyLoadedAsync(CancellationToken cancellationToken) { + Assumes.Present(_menuCommandService); + // we need to load it as early as possible since we can have errors from // package from each language very early await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); - await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); - await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); - await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); + await LoadAnalyzerNodeComponentsAsync(_menuCommandService, cancellationToken).ConfigureAwait(false); - await LoadAnalyzerNodeComponentsAsync(cancellationToken).ConfigureAwait(false); - - await LoadStackTraceExplorerMenusAsync(cancellationToken).ConfigureAwait(false); + // Ensure the stack trace explorer handler is created so it subscribes to broadcast messages + // if the "open on focus" option is enabled. + await ComponentModel.GetService().EnsureInitializedAsync(cancellationToken).ConfigureAwait(false); // Initialize keybinding reset detector await ComponentModel.DefaultExportProvider.GetExportedValue().InitializeAsync(cancellationToken).ConfigureAwait(false); @@ -137,16 +163,6 @@ protected override string GetToolWindowTitle(Type toolWindowType, int id) protected override Task InitializeToolWindowAsync(Type toolWindowType, int id, CancellationToken cancellationToken) => Task.FromResult((object?)null); - private async Task LoadStackTraceExplorerMenusAsync(CancellationToken cancellationToken) - { - // Obtain services and QueryInterface from the main thread - await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - - var menuCommandService = (OleMenuCommandService?)await GetServiceAsync(typeof(IMenuCommandService)).ConfigureAwait(true); - Assumes.Present(menuCommandService); - StackTraceExplorerCommandHandler.Initialize(menuCommandService, this); - } - protected override void Dispose(bool disposing) { UnregisterAnalyzerTracker(); @@ -167,9 +183,9 @@ private static void ReportSessionWideTelemetry() FeaturesSessionTelemetry.Report(); } - private async Task LoadAnalyzerNodeComponentsAsync(CancellationToken cancellationToken) + private async Task LoadAnalyzerNodeComponentsAsync(ThreadSafeMenuCommandService menuCommandService, CancellationToken cancellationToken) { - await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); + await this.ComponentModel.GetService().InitializeAsync(this, menuCommandService, cancellationToken).ConfigureAwait(false); _ruleSetEventHandler = this.ComponentModel.GetService(); if (_ruleSetEventHandler != null) diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.resx b/src/VisualStudio/Core/Def/ServicesVSResources.resx index d9dc9250bde2..c92b32f98fc0 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.resx +++ b/src/VisualStudio/Core/Def/ServicesVSResources.resx @@ -1889,6 +1889,12 @@ Additional information: {1} Experimental feature This is used by the settings UI to mark experimental features. + + Logging Roslyn Workspace structure... + + + Visual Studio + Prefer simple property accessors @@ -1906,4 +1912,11 @@ Additional information: {1} Warning {0} does not bind to type {0} is a type name + + Include Roslyn source-generated files + Used in the unified settings registration for real-time test discovery + + + Show Inheritance + \ No newline at end of file diff --git a/src/VisualStudio/Core/Def/StackTraceExplorer/StackTraceExplorerCommandHandler.cs b/src/VisualStudio/Core/Def/StackTraceExplorer/StackTraceExplorerCommandHandler.cs index 0559ac07a49d..95cacbde27d1 100644 --- a/src/VisualStudio/Core/Def/StackTraceExplorer/StackTraceExplorerCommandHandler.cs +++ b/src/VisualStudio/Core/Def/StackTraceExplorer/StackTraceExplorerCommandHandler.cs @@ -3,9 +3,12 @@ // See the LICENSE file in the project root for more information. using System; -using System.ComponentModel.Design; +using System.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.StackTraceExplorer; @@ -16,25 +19,37 @@ namespace Microsoft.VisualStudio.LanguageServices.StackTraceExplorer; -internal sealed class StackTraceExplorerCommandHandler : IVsBroadcastMessageEvents, IDisposable +[Export(typeof(StackTraceExplorerCommandHandler))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class StackTraceExplorerCommandHandler( + IThreadingContext threadingContext, + IGlobalOptionService globalOptions, + [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) : IVsBroadcastMessageEvents, IDisposable { - private readonly RoslynPackage _package; - private readonly IThreadingContext _threadingContext; - private readonly IGlobalOptionService _globalOptions; - private static StackTraceExplorerCommandHandler? _instance; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IGlobalOptionService _globalOptions = globalOptions; + private readonly IServiceProvider _serviceProvider = serviceProvider; private uint _vsShellBroadcastCookie; + private bool _initialized; - private StackTraceExplorerCommandHandler(RoslynPackage package) + /// + /// Called during solution load to ensure the handler is created and subscribes to broadcast + /// messages if the "open on focus" option is enabled. + /// + internal async Task EnsureInitializedAsync(CancellationToken cancellationToken) { - _package = package; - _threadingContext = package.ComponentModel.GetService(); - _globalOptions = package.ComponentModel.GetService(); + if (_initialized) + return; + + _initialized = true; _globalOptions.AddOptionChangedHandler(this, GlobalOptionChanged); var enabled = _globalOptions.GetOption(StackTraceExplorerOptionsStorage.OpenOnFocus); if (enabled) { + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); AdviseBroadcastMessages(); } } @@ -55,10 +70,10 @@ public int OnBroadcastMessage(uint msg, IntPtr wParam, IntPtr lParam) return VSConstants.S_OK; } - var window = GetOrInitializeWindow(); _threadingContext.JoinableTaskFactory.RunAsync(async () => { - var shouldActivate = await window.ShouldShowOnActivatedAsync(default).ConfigureAwait(false); + var window = await GetOrInitializeWindowAsync().ConfigureAwait(true); + var shouldActivate = await window.ShouldShowOnActivatedAsync(default).ConfigureAwait(true); if (shouldActivate) { @@ -76,7 +91,8 @@ public void Dispose() { UnadviseBroadcastMessages(); - _globalOptions.RemoveOptionChangedHandler(this, GlobalOptionChanged); + if (_initialized) + _globalOptions.RemoveOptionChangedHandler(this, GlobalOptionChanged); } private void AdviseBroadcastMessages() @@ -86,9 +102,7 @@ private void AdviseBroadcastMessages() return; } - var serviceProvider = (IServiceProvider)_package; - var vsShell = serviceProvider.GetService(typeof(SVsShell)) as IVsShell; - + var vsShell = _serviceProvider.GetService(typeof(SVsShell)) as IVsShell; vsShell?.AdviseBroadcastMessages(this, out _vsShellBroadcastCookie); } @@ -96,8 +110,7 @@ private void UnadviseBroadcastMessages() { if (_vsShellBroadcastCookie != 0) { - var serviceProvider = (IServiceProvider)_package; - var vsShell = serviceProvider.GetService(typeof(SVsShell)) as IVsShell; + var vsShell = _serviceProvider.GetService(typeof(SVsShell)) as IVsShell; if (vsShell is not null) { vsShell.UnadviseBroadcastMessages(_vsShellBroadcastCookie); @@ -131,11 +144,11 @@ private void GlobalOptionChanged(object sender, object target, OptionChangedEven } } - private void Execute(object sender, EventArgs e) + internal void OnExecute(object sender, EventArgs e) { ThreadHelper.ThrowIfNotOnUIThread(); - var window = GetOrInitializeWindow(); + var window = _threadingContext.JoinableTaskFactory.Run(() => GetOrInitializeWindowAsync()); var windowFrame = (IVsWindowFrame)window.Frame; ErrorHandler.ThrowOnFailure(windowFrame.Show()); @@ -145,58 +158,34 @@ private void Execute(object sender, EventArgs e) window.Root?.ViewModel.DoPasteSynchronously(default); } - private StackTraceExplorerToolWindow GetOrInitializeWindow() + private async Task GetOrInitializeWindowAsync() { + var package = await RoslynPackage.GetOrLoadAsync(_threadingContext, (IAsyncServiceProvider)_serviceProvider, _threadingContext.DisposalToken).ConfigureAwait(true); + Contract.ThrowIfNull(package); + // Get the instance number 0 of this tool window. This window is single instance so this instance // is actually the only one. // The last flag is set to true so that if the tool window does not exists it will be created. - var window = _package.FindToolWindow(typeof(StackTraceExplorerToolWindow), 0, true) as StackTraceExplorerToolWindow; + var window = package.FindToolWindow(typeof(StackTraceExplorerToolWindow), 0, true) as StackTraceExplorerToolWindow; if (window is not { Frame: not null }) { throw new NotSupportedException("Cannot create tool window"); } - window.InitializeIfNeeded(_package); + window.InitializeIfNeeded(package); return window; } - private void Paste(object sender, EventArgs e) + internal void OnPaste(object sender, EventArgs e) { - RoslynDebug.AssertNotNull(_instance); - - var window = _instance.GetOrInitializeWindow(); + var window = _threadingContext.JoinableTaskFactory.Run(() => GetOrInitializeWindowAsync()); window.Root?.ViewModel?.DoPasteSynchronously(default); } - private void Clear(object sender, EventArgs e) + internal void OnClear(object sender, EventArgs e) { - RoslynDebug.AssertNotNull(_instance); - - var window = _instance.GetOrInitializeWindow(); + var window = _threadingContext.JoinableTaskFactory.Run(() => GetOrInitializeWindowAsync()); window.Root?.OnClear(); } - - internal static void Initialize(OleMenuCommandService menuCommandService, RoslynPackage package) - { - if (_instance is not null) - { - return; - } - - _instance = new(package); - - var menuCommandId = new CommandID(Guids.StackTraceExplorerCommandId, 0x0100); - var menuItem = new MenuCommand(_instance.Execute, menuCommandId); - menuCommandService.AddCommand(menuItem); - - var pasteCommandId = new CommandID(Guids.StackTraceExplorerCommandId, 0x0101); - var clearCommandId = new CommandID(Guids.StackTraceExplorerCommandId, 0x0102); - - var pasteMenuItem = new MenuCommand(_instance.Paste, pasteCommandId); - var clearMenuItem = new MenuCommand(_instance.Clear, clearCommandId); - - menuCommandService.AddCommand(pasteMenuItem); - menuCommandService.AddCommand(clearMenuItem); - } } diff --git a/src/VisualStudio/Core/Def/SyncNamespaces/SyncNamespacesCommandHandler.cs b/src/VisualStudio/Core/Def/SyncNamespaces/SyncNamespacesCommandHandler.cs index d6d2e4d743f3..18c338a85e91 100644 --- a/src/VisualStudio/Core/Def/SyncNamespaces/SyncNamespacesCommandHandler.cs +++ b/src/VisualStudio/Core/Def/SyncNamespaces/SyncNamespacesCommandHandler.cs @@ -4,11 +4,9 @@ using System; using System.Collections.Immutable; -using System.ComponentModel.Design; -using System.Composition; +using System.ComponentModel.Composition; using System.Linq; using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; @@ -25,42 +23,29 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.SyncNamespaces; -[Export(typeof(SyncNamespacesCommandHandler)), Shared] +[Export(typeof(SyncNamespacesCommandHandler))] internal sealed class SyncNamespacesCommandHandler { private readonly VisualStudioWorkspace _workspace; private readonly IUIThreadOperationExecutor _threadOperationExecutor; private readonly IThreadingContext _threadingContext; - private IServiceProvider? _serviceProvider; + private readonly IServiceProvider _serviceProvider; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public SyncNamespacesCommandHandler( IUIThreadOperationExecutor threadOperationExecutor, VisualStudioWorkspace workspace, - IThreadingContext threadingContext) + IThreadingContext threadingContext, + [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) { _threadOperationExecutor = threadOperationExecutor; _workspace = workspace; _threadingContext = threadingContext; + _serviceProvider = serviceProvider; } - public async Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken) - { - Contract.ThrowIfNull(serviceProvider); - - _serviceProvider = (IServiceProvider)serviceProvider; - - // Hook up the "Remove Unused References" menu command for CPS based managed projects. - var menuCommandService = await serviceProvider.GetServiceAsync(throwOnFailure: false, cancellationToken).ConfigureAwait(false); - if (menuCommandService != null) - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.SyncNamespaces, Guids.RoslynGroupId, OnSyncNamespacesForSelectedProject, OnSyncNamespacesForSelectedProjectStatus); - } - } - - private void OnSyncNamespacesForSelectedProjectStatus(object sender, EventArgs e) + internal void OnSyncNamespacesForSelectedProjectStatus(object sender, EventArgs e) { var command = (OleMenuCommand)sender; @@ -91,7 +76,7 @@ private void OnSyncNamespacesForSelectedProjectStatus(object sender, EventArgs e } } - private void OnSyncNamespacesForSelectedProject(object sender, EventArgs args) + internal void OnSyncNamespacesForSelectedProject(object sender, EventArgs args) { if (VisualStudioCommandHandlerHelpers.TryGetSelectedProjectHierarchy(_serviceProvider, out var projectHierarchy)) { diff --git a/src/VisualStudio/Core/Def/TaskList/ExternalErrorDiagnosticUpdateSource.cs b/src/VisualStudio/Core/Def/TaskList/ExternalErrorDiagnosticUpdateSource.cs index b5f9eb0dc6ee..98e84a5d3202 100644 --- a/src/VisualStudio/Core/Def/TaskList/ExternalErrorDiagnosticUpdateSource.cs +++ b/src/VisualStudio/Core/Def/TaskList/ExternalErrorDiagnosticUpdateSource.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel.Composition; using System.Diagnostics; @@ -20,7 +19,7 @@ using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Threading; using Microsoft.ServiceHub.Framework; -using Microsoft.VisualStudio.Debugger.ComponentInterfaces; +using Microsoft.VisualStudio.LanguageServices.TaskList; using Microsoft.VisualStudio.RpcContracts.DiagnosticManagement; using Microsoft.VisualStudio.RpcContracts.Utilities; using Microsoft.VisualStudio.Shell; @@ -41,6 +40,7 @@ internal sealed class ExternalErrorDiagnosticUpdateSource : IDisposable { private readonly Workspace _workspace; private readonly IServiceBroker _serviceBroker; + private readonly VisualStudioDiagnosticIdCache _diagnosticCache; /// /// Task queue to serialize all the work for errors reported by build. @@ -69,6 +69,7 @@ public ExternalErrorDiagnosticUpdateSource( IThreadingContext threadingContext) { _workspace = workspace; + _diagnosticCache = workspace.Services.GetRequiredService(); _serviceBroker = serviceBroker; _taskQueue = new AsyncBatchingWorkQueue>( @@ -118,13 +119,13 @@ public void Dispose() /// for the given during the current build in progress. /// This API is only intended to be invoked from while a build is in progress. /// - public async Task IsSupportedDiagnosticIdAsync(ProjectId projectId, string id, CancellationToken cancellationToken) + public bool IsUnsupportedDiagnosticId(ProjectId projectId, string id) { var state = GetBuildInProgressState(); if (state is null) - return false; + return true; - return await state.IsSupportedDiagnosticIdAsync(projectId, id, cancellationToken).ConfigureAwait(false); + return state.IsUnsupportedDiagnosticId(projectId, id); } public void ClearErrors(ProjectId projectId) @@ -141,6 +142,10 @@ public void ClearErrors(ProjectId projectId) /// internal void OnSolutionBuildStarted() { + // We want the diagnostic cache to be up-to-date when building so that we + // can correctly report unsupported diagnostic ids back to VS. + _diagnosticCache.Refresh(); + _ = GetOrCreateInProgressState(); _taskQueue.AddWork(async cancellationToken => @@ -317,49 +322,31 @@ private InProgressState GetOrCreateInProgressState() // We take current snapshot of solution when the state is first created. and through out this code, we use this snapshot. // Since we have no idea what actual snapshot of solution the out of proc build has picked up, it doesn't remove the race we can have // between build and diagnostic service, but this at least make us to consistent inside of our code. - _stateDoNotAccessDirectly ??= new InProgressState(_workspace.CurrentSolution); + _stateDoNotAccessDirectly ??= new InProgressState(_workspace.CurrentSolution, _diagnosticCache); return _stateDoNotAccessDirectly; } } - private sealed class InProgressState(Solution solution) + private sealed class InProgressState(Solution solution, VisualStudioDiagnosticIdCache diagnosticIdCache) { - /// - /// Map from project ID to all the possible analyzer diagnostic IDs that can be reported in the project. - /// - private ImmutableDictionary>> _allDiagnosticIdMap = ImmutableDictionary>>.Empty; - public Solution Solution { get; } = solution; - public async Task IsSupportedDiagnosticIdAsync(ProjectId projectId, string id, CancellationToken cancellationToken) + public bool IsUnsupportedDiagnosticId(ProjectId projectId, string id) { - var lazyIds = _allDiagnosticIdMap.TryGetValue(projectId, out var temp) - ? temp - : GetLazyIdsSlow(); - - var ids = await lazyIds.GetValueAsync(cancellationToken).ConfigureAwait(false); - return ids.Contains(id); + var project = Solution.GetProject(projectId); + if (project is null) + { + return true; + } - AsyncLazy> GetLazyIdsSlow() + if (diagnosticIdCache.TryGetDiagnosticIds(projectId, out var supportedIds)) { - return ImmutableInterlocked.GetOrAdd(ref _allDiagnosticIdMap, projectId, projectId => AsyncLazy.Create(async cancellationToken => - { - var project = Solution.GetProject(projectId); - if (project == null) - { - // projectId no longer exist - return []; - } - - // set ids set - var builder = ImmutableHashSet.CreateBuilder(); - var service = this.Solution.Services.GetRequiredService(); - var descriptorMap = await service.GetDiagnosticDescriptorsPerReferenceAsync(project, cancellationToken).ConfigureAwait(false); - builder.UnionWith(descriptorMap.Values.SelectMany(v => v.Select(d => d.Id))); - - return builder.ToImmutable(); - })); + return !supportedIds.Contains(id); } + + // The cache hasn't been populated yet. We will report false because we do not know + // for certain that we do not support the diagnostic id. + return false; } } } diff --git a/src/VisualStudio/Core/Def/TaskList/ProjectExternalErrorReporter.cs b/src/VisualStudio/Core/Def/TaskList/ProjectExternalErrorReporter.cs index e0519e60ab8a..b645e3228b81 100644 --- a/src/VisualStudio/Core/Def/TaskList/ProjectExternalErrorReporter.cs +++ b/src/VisualStudio/Core/Def/TaskList/ProjectExternalErrorReporter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Diagnostics; @@ -200,21 +199,27 @@ public void ReportError2( int iEndColumn, string bstrFileName) { - // make sure we have error id, otherwise, we simple don't support // this error - if (bstrErrorId == null) + if (string.IsNullOrEmpty(bstrErrorId)) { - // record NFW to see who violates contract. - FatalError.ReportAndCatch(new Exception("errorId is null")); - return; + if (bstrErrorId is null) + { + // record NFW to see who violates contract. + FatalError.ReportAndCatch(new Exception("errorId is null")); + } + + throw new NotImplementedException(); } - if (!bstrErrorId.StartsWith(_errorCodePrefix)) - return; + if (!bstrErrorId.StartsWith(_errorCodePrefix) && + DiagnosticProvider.IsUnsupportedDiagnosticId(_projectId, bstrErrorId)) + { + throw new NotImplementedException(); + } if ((iEndLine >= 0 && iEndColumn >= 0) && - ((iEndLine < iStartLine) || + ((iEndLine < iStartLine) || (iEndLine == iStartLine && iEndColumn < iStartColumn))) { throw new ArgumentException(ServicesVSResources.End_position_must_be_start_position); diff --git a/src/VisualStudio/Core/Def/TaskList/VisualStudioDiagnosticIdCache.cs b/src/VisualStudio/Core/Def/TaskList/VisualStudioDiagnosticIdCache.cs new file mode 100644 index 000000000000..365a8455ba46 --- /dev/null +++ b/src/VisualStudio/Core/Def/TaskList/VisualStudioDiagnosticIdCache.cs @@ -0,0 +1,193 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Legacy; + +namespace Microsoft.VisualStudio.LanguageServices.TaskList; + +[ExportWorkspaceServiceFactory(typeof(VisualStudioDiagnosticIdCache)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VisualStudioDiagnosticIdCacheFactory( + IThreadingContext threadingContext, + IAsynchronousOperationListenerProvider listenerProvider) : IWorkspaceServiceFactory +{ + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + return new VisualStudioDiagnosticIdCache( + workspaceServices.Workspace, + threadingContext, + listenerProvider); + } +} + +internal class VisualStudioDiagnosticIdCache : IWorkspaceService +{ + /// + /// This dictionary maps ProjectIds to a set of DiagnosticIds. + /// + /// + /// A being in the map means we are tracking changes for this project + /// and will update diagnostic ids when AnalyzerReferences change. A null value + /// means that we haven't computed the diagnostic ids for this project id yet. + /// + private readonly ConcurrentDictionary?> _projectIdToDiagnosticIdsCache = []; + private HashSet _projectIdsToRefresh = []; + private Task _refreshTask = Task.CompletedTask; + private readonly object _gate = new(); + + private readonly Workspace _workspace; + private readonly IDiagnosticAnalyzerService _analyzerService; + private readonly IThreadingContext _threadingContext; + private readonly IAsynchronousOperationListener _listener; + + public VisualStudioDiagnosticIdCache( + Workspace workspace, + IThreadingContext threadingContext, + IAsynchronousOperationListenerProvider listenerProvider) + { + _workspace = workspace; + _analyzerService = workspace.Services.GetRequiredService(); + _threadingContext = threadingContext; + _listener = listenerProvider.GetListener(FeatureAttribute.DiagnosticService); + + _workspace.RegisterWorkspaceChangedHandler(WorkspaceChanged); + } + + /// + /// We will only cache diagnostic ids for projects which have been registered by the . + /// + public void RegisterProject(ProjectId projectId) + { + lock (_gate) + { + // Ensure we have an entry for this projectId in case we get a workspace change event before + // we set it in RefreshCacheDiagnosticIdsAsync. + _projectIdToDiagnosticIdsCache.TryAdd(projectId, null); + _projectIdsToRefresh.Add(projectId); + } + } + + public void Refresh() + { + lock (_gate) + { + var projectIdsToRefresh = _projectIdsToRefresh; + _projectIdsToRefresh = []; + + if (projectIdsToRefresh.Count == 0) + { + return; + } + + var refreshToken = _listener.BeginAsyncOperation(nameof(Refresh)); + _refreshTask = _refreshTask.ContinueWith( + async _ => await RefreshCachedDiagnosticIdsAsync(projectIdsToRefresh, _threadingContext.DisposalToken).ConfigureAwait(false), + _threadingContext.DisposalToken, + TaskContinuationOptions.None, + TaskScheduler.Default) + .Unwrap() + .CompletesAsyncOperation(refreshToken); + } + } + + public bool TryGetDiagnosticIds(ProjectId projectId, [NotNullWhen(returnValue: true)] out ImmutableHashSet? diagnosticIds) + => _projectIdToDiagnosticIdsCache.TryGetValue(projectId, out diagnosticIds) && diagnosticIds != null; + + private void WorkspaceChanged(WorkspaceChangeEventArgs e) + { + if (_projectIdToDiagnosticIdsCache.IsEmpty) + { + return; + } + + var workspaceChanges = e.NewSolution.GetChanges(e.OldSolution); + + foreach (var removedProject in workspaceChanges.GetRemovedProjects()) + { + if (_projectIdToDiagnosticIdsCache.ContainsKey(removedProject.Id)) + { + lock (_gate) + { + // Avoid a race condition where we remove a project here only for it to be added back + // by a refresh operation which is already in flight. Queue a refresh for this project + // and remove it when refreshing. + _projectIdsToRefresh.Add(removedProject.Id); + } + } + } + + foreach (var projectChange in workspaceChanges.GetProjectChanges()) + { + if (_projectIdToDiagnosticIdsCache.ContainsKey(projectChange.ProjectId)) + { + var oldProject = projectChange.OldProject; + var newProject = projectChange.NewProject; + + var analyzersChanged = !oldProject.AnalyzerReferences.Equals(newProject.AnalyzerReferences); + if (analyzersChanged) + { + lock (_gate) + { + _projectIdsToRefresh.Add(projectChange.NewProject.Id); + } + } + } + } + } + + private async ValueTask RefreshCachedDiagnosticIdsAsync( + IEnumerable projectIds, + CancellationToken cancellationToken) + { + var solution = _workspace.CurrentSolution; + var builder = ImmutableArray.CreateBuilder(); + foreach (var projectId in projectIds) + { + if (!solution.ContainsProject(projectId)) + { + _projectIdToDiagnosticIdsCache.TryRemove(projectId, out _); + continue; + } + + builder.Add(projectId); + } + + if (builder.Count == 0) + { + return; + } + + var projectIdToDiagnosticIdsMap = await _analyzerService.GetAllDiagnosticIdsAsync( + solution, + builder.ToImmutable(), + cancellationToken).ConfigureAwait(false); + + foreach (var projectIdToDiagnosticIds in projectIdToDiagnosticIdsMap) + { + _projectIdToDiagnosticIdsCache[projectIdToDiagnosticIds.Key] = projectIdToDiagnosticIds.Value; + } + } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(VisualStudioDiagnosticIdCache diagnosticCache) + { + public int RegisteredProjectCount => diagnosticCache._projectIdToDiagnosticIdsCache.Count; + } +} diff --git a/src/VisualStudio/Core/Def/UnifiedSettings/roslynSettings.registration.json b/src/VisualStudio/Core/Def/UnifiedSettings/roslynSettings.registration.json new file mode 100644 index 000000000000..568a0d0eb638 --- /dev/null +++ b/src/VisualStudio/Core/Def/UnifiedSettings/roslynSettings.registration.json @@ -0,0 +1,18 @@ +// NOTE: +// When this file is changed. Please also update the cache tag under settings entry in src/VisualStudio/Core/Def/PackageRegistration.pkgdef +// Otherwise your change might be ignored. +// See https://devdiv.visualstudio.com/DevDiv/_wiki/wikis/DevDiv.wiki/39345/Manifest-Build-Deployment-and-Setup-Authoring-In-Depth?anchor=example-pkgdef-key for more details +{ + "properties": { + "test.includeSourceGeneratedFilesInRealTimeDiscovery": { + "title": "@Include_Roslyn_source_generated_files;..\\Microsoft.VisualStudio.LanguageServices.dll", + "type": "boolean", + "default": true, + "enableWhen": "${config:test.realTimeDiscovery} == 'true'", + "alternateDefault": { + "flagName": "UnitTesting.DisableRoslynSourceGeneratedFiles", + "default": false + } + } + } +} \ No newline at end of file diff --git a/src/VisualStudio/Core/Def/UnusedReferences/RemoveUnusedReferencesCommandHandler.cs b/src/VisualStudio/Core/Def/UnusedReferences/RemoveUnusedReferencesCommandHandler.cs index be81d4d22de9..bb3401bf461f 100644 --- a/src/VisualStudio/Core/Def/UnusedReferences/RemoveUnusedReferencesCommandHandler.cs +++ b/src/VisualStudio/Core/Def/UnusedReferences/RemoveUnusedReferencesCommandHandler.cs @@ -4,8 +4,7 @@ using System; using System.Collections.Immutable; -using System.ComponentModel.Design; -using System.Composition; +using System.ComponentModel.Composition; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; @@ -28,7 +27,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.UnusedReferences; -[Export(typeof(RemoveUnusedReferencesCommandHandler)), Shared] +[Export(typeof(RemoveUnusedReferencesCommandHandler))] internal sealed class RemoveUnusedReferencesCommandHandler { private const string ProjectAssetsFilePropertyName = "ProjectAssetsFile"; @@ -37,7 +36,7 @@ internal sealed class RemoveUnusedReferencesCommandHandler private readonly VisualStudioWorkspace _workspace; private readonly IGlobalOptionService _globalOptions; private readonly IUIThreadOperationExecutor _threadOperationExecutor; - private IServiceProvider? _serviceProvider; + private readonly IServiceProvider _serviceProvider; private IReferenceCleanupService ReferenceCleanupService => _workspace.Services.GetRequiredService(); @@ -49,31 +48,18 @@ public RemoveUnusedReferencesCommandHandler( RemoveUnusedReferencesDialogProvider unusedReferenceDialogProvider, IUIThreadOperationExecutor threadOperationExecutor, VisualStudioWorkspace workspace, - IGlobalOptionService globalOptions) + IGlobalOptionService globalOptions, + [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider) { _threadingContext = threadingContext; _unusedReferenceDialogProvider = unusedReferenceDialogProvider; _threadOperationExecutor = threadOperationExecutor; _workspace = workspace; _globalOptions = globalOptions; + _serviceProvider = serviceProvider; } - public async Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken) - { - Contract.ThrowIfNull(serviceProvider); - - _serviceProvider = (IServiceProvider)serviceProvider; - - // Hook up the "Remove Unused References" menu command for CPS based managed projects. - var menuCommandService = await serviceProvider.GetServiceAsync(throwOnFailure: false, cancellationToken).ConfigureAwait(false); - if (menuCommandService != null) - { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - VisualStudioCommandHandlerHelpers.AddCommand(menuCommandService, ID.RoslynCommands.RemoveUnusedReferences, Guids.RoslynGroupId, OnRemoveUnusedReferencesForSelectedProject, OnRemoveUnusedReferencesForSelectedProjectStatus); - } - } - - private void OnRemoveUnusedReferencesForSelectedProjectStatus(object sender, EventArgs e) + internal void OnRemoveUnusedReferencesForSelectedProjectStatus(object sender, EventArgs e) { var command = (OleMenuCommand)sender; @@ -105,7 +91,7 @@ private void OnRemoveUnusedReferencesForSelectedProjectStatus(object sender, Eve } } - private void OnRemoveUnusedReferencesForSelectedProject(object sender, EventArgs args) + internal void OnRemoveUnusedReferencesForSelectedProject(object sender, EventArgs args) { if (VisualStudioCommandHandlerHelpers.TryGetSelectedProjectHierarchy(_serviceProvider, out var hierarchy)) { diff --git a/src/VisualStudio/Core/Def/Utilities/ThreadSafeMenuCommandService.cs b/src/VisualStudio/Core/Def/Utilities/ThreadSafeMenuCommandService.cs new file mode 100644 index 000000000000..945c633b5be4 --- /dev/null +++ b/src/VisualStudio/Core/Def/Utilities/ThreadSafeMenuCommandService.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.ComponentModel.Design; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; + +/// +/// Thread-safe wrapper around that serializes +/// all command registrations through a lock so the underlying service's lack of thread +/// safety isn't a problem. It can still be used on any thread then, as long as nobody else +/// is using it directly. +/// +internal sealed class ThreadSafeMenuCommandService +{ + private readonly IMenuCommandService _menuCommandService; + private readonly object _gate = new(); + + public ThreadSafeMenuCommandService(IMenuCommandService menuCommandService) + { + _menuCommandService = menuCommandService; + } + + /// + /// Adds an with both an invoke and a before-query-status handler. + /// + public OleMenuCommand AddCommand( + Guid commandGroup, + int commandId, + EventHandler invokeHandler, + EventHandler beforeQueryStatus) + { + var commandIdWithGroupId = new CommandID(commandGroup, commandId); + var command = new OleMenuCommand(invokeHandler, delegate { }, beforeQueryStatus, commandIdWithGroupId); + lock (_gate) + { + _menuCommandService.AddCommand(command); + } + + return command; + } + + /// + /// Adds a simple with only an invoke handler. + /// + public MenuCommand AddCommand(Guid commandGroup, int commandId, EventHandler invokeHandler) + { + var commandIdWithGroupId = new CommandID(commandGroup, commandId); + var command = new MenuCommand(invokeHandler, commandIdWithGroupId); + lock (_gate) + { + _menuCommandService.AddCommand(command); + } + + return command; + } +} diff --git a/src/VisualStudio/Core/Def/Utilities/VisualStudioCommandHandlerHelpers.cs b/src/VisualStudio/Core/Def/Utilities/VisualStudioCommandHandlerHelpers.cs index d35374d65601..fe7198e7caed 100644 --- a/src/VisualStudio/Core/Def/Utilities/VisualStudioCommandHandlerHelpers.cs +++ b/src/VisualStudio/Core/Def/Utilities/VisualStudioCommandHandlerHelpers.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.ComponentModel.Design; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; @@ -14,19 +13,6 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; internal static class VisualStudioCommandHandlerHelpers { - public static OleMenuCommand AddCommand( - IMenuCommandService menuCommandService, - int commandId, - Guid commandGroup, - EventHandler invokeHandler, - EventHandler beforeQueryStatus) - { - var commandIdWithGroupId = new CommandID(commandGroup, commandId); - var command = new OleMenuCommand(invokeHandler, delegate { }, beforeQueryStatus, commandIdWithGroupId); - menuCommandService.AddCommand(command); - return command; - } - public static bool TryGetSelectedProjectHierarchy(IServiceProvider? serviceProvider, [NotNullWhen(returnValue: true)] out IVsHierarchy? hierarchy) { hierarchy = null; diff --git a/src/VisualStudio/Core/Def/Workspace/VisualStudioIHostWorkspaceProvider.cs b/src/VisualStudio/Core/Def/Workspace/VisualStudioIHostWorkspaceProvider.cs index 766260fb59b8..176892156ae6 100644 --- a/src/VisualStudio/Core/Def/Workspace/VisualStudioIHostWorkspaceProvider.cs +++ b/src/VisualStudio/Core/Def/Workspace/VisualStudioIHostWorkspaceProvider.cs @@ -12,14 +12,9 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation; [Shared] [Export(typeof(IHostWorkspaceProvider))] -internal sealed class VisualStudioIHostWorkspaceProvider : IHostWorkspaceProvider +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VisualStudioIHostWorkspaceProvider(VisualStudioWorkspace workspace) : IHostWorkspaceProvider { - public Workspace Workspace { get; } - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public VisualStudioIHostWorkspaceProvider(VisualStudioWorkspace workspace) - { - Workspace = workspace; - } + public Workspace Workspace { get; } = workspace; } diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.cs.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.cs.xlf index 6fdb0af8371b..db5c9091e05d 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.cs.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.cs.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Protokolovat strukturu pracovního prostoru Roslyn + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... &Analyzátor... @@ -587,6 +602,16 @@ Přejít na implementaci + + Show Inheritance + Zobrazit dědičnost + + + + Show Inheritance + Zobrazit dědičnost + + Find All References Najít všechny odkazy diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.de.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.de.xlf index 79f414c09a37..3f15a0edf959 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.de.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.de.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Roslyn-Arbeitsbereichsstruktur protokollieren + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... &Analysetool... @@ -587,6 +602,16 @@ Zur Implementierung wechseln + + Show Inheritance + Vererbung anzeigen + + + + Show Inheritance + Vererbung anzeigen + + Find All References Alle Verweise suchen diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.es.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.es.xlf index 8698f9b17ff4..2b9a3c6971f0 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.es.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.es.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Estructura del área de trabajo de Roslyn de registro + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... &Analizador... @@ -587,6 +602,16 @@ Ir a la implementación + + Show Inheritance + Mostrar herencia + + + + Show Inheritance + Mostrar herencia + + Find All References Buscar todas las referencias diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.fr.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.fr.xlf index fc2838bc64dd..01d6a02af7a3 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.fr.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.fr.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Journaliser la structure de l’espace de travail Roslyn + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... &Analyseur... @@ -587,6 +602,16 @@ Accéder à l'implémentation + + Show Inheritance + Afficher l’héritage + + + + Show Inheritance + Afficher l’héritage + + Find All References Rechercher toutes les références diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.it.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.it.xlf index 9ccaf930b166..9cba72f31550 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.it.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.it.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Registra la struttura dell'area di lavoro Roslyn + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... &Analizzatore... @@ -587,6 +602,16 @@ Vai all'implementazione + + Show Inheritance + Mostra ereditarietà + + + + Show Inheritance + Mostra ereditarietà + + Find All References Trova tutti i riferimenti diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.ja.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.ja.xlf index af46ec9a654f..be8875fa1897 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.ja.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.ja.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Roslyn ワークスペース構造をログに記録する + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... アナライザー(&A)... @@ -587,6 +602,16 @@ 実装に移動 + + Show Inheritance + 継承を表示 + + + + Show Inheritance + 継承を表示 + + Find All References 参照をすべて検索 diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.ko.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.ko.xlf index f35779c1121f..112fc2ae2f15 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.ko.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.ko.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Roslyn 작업 영역 구조 기록 + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... 분석기(&A)... @@ -587,6 +602,16 @@ 구현으로 이동 + + Show Inheritance + 상속 표시 + + + + Show Inheritance + 상속 표시 + + Find All References 모든 참조 찾기 diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.pl.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.pl.xlf index 804cf5c88d9b..aeff52a9522a 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.pl.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.pl.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Rejestruj strukturę obszaru roboczego Roslyn + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... &Analizator... @@ -587,6 +602,16 @@ Przejdź do implementacji + + Show Inheritance + Pokaż dziedziczenie + + + + Show Inheritance + Pokaż dziedziczenie + + Find All References Znajdź wszystkie odwołania diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.pt-BR.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.pt-BR.xlf index b6bd18b19f72..5fb30e1cab2a 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.pt-BR.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.pt-BR.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Estrutura do Workspace do Log Roslyn + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... &Analisador... @@ -587,6 +602,16 @@ Ir para Implementação + + Show Inheritance + Mostrar Herança + + + + Show Inheritance + Mostrar Herança + + Find All References Localizar Todas as Referências diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.ru.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.ru.xlf index 91d44a0c2fda..eb7d6a0be7b3 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.ru.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.ru.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Регистрировать структуру рабочей области Roslyn + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... &Анализатор... @@ -587,6 +602,16 @@ Перейти к реализации + + Show Inheritance + Показать наследование + + + + Show Inheritance + Показать наследование + + Find All References Найти все ссылки diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.tr.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.tr.xlf index 614ed19b8402..d28fdf23bd5f 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.tr.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.tr.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + Roslyn Çalışma Alanı Yapısını Günlüğe Kaydet + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... Çözü&mleyici... @@ -587,6 +602,16 @@ Uygulamaya Git + + Show Inheritance + Devralmayı Göster + + + + Show Inheritance + Devralmayı Göster + + Find All References Tüm Başvuruları Bul diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.zh-Hans.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.zh-Hans.xlf index 8de79c129abb..020cdf099694 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.zh-Hans.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.zh-Hans.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + 记录 Roslyn 工作区结构 + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... 分析器(&A)... @@ -587,6 +602,16 @@ 转到实现 + + Show Inheritance + 显示继承 + + + + Show Inheritance + 显示继承 + + Find All References 查找全部引用 diff --git a/src/VisualStudio/Core/Def/xlf/Commands.vsct.zh-Hant.xlf b/src/VisualStudio/Core/Def/xlf/Commands.vsct.zh-Hant.xlf index da1c17ee7b05..c40a2ff5d44c 100644 --- a/src/VisualStudio/Core/Def/xlf/Commands.vsct.zh-Hant.xlf +++ b/src/VisualStudio/Core/Def/xlf/Commands.vsct.zh-Hant.xlf @@ -307,6 +307,21 @@ Warning + + Log Roslyn Workspace Structure + 記錄 Roslyn 工作區結構 + + + + LogRoslynWorkspaceStructure + LogRoslynWorkspaceStructure + + + + Project.LogRoslynWorkspaceStructure + Project.LogRoslynWorkspaceStructure + + &Analyzer... 分析器(&A)... @@ -587,6 +602,16 @@ 前往實作 + + Show Inheritance + 顯示繼承 + + + + Show Inheritance + 顯示繼承 + + Find All References 尋找所有參考資料 diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.cs.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.cs.xlf index f047b4c484cf..9d656f9b6aa1 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.cs.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.cs.xlf @@ -572,6 +572,11 @@ V jiných operátorech + + Include Roslyn source-generated files + Zahrnout soubory generované ze zdrojového kódu nástrojem Roslyn + Used in the unified settings registration for real-time test discovery + Include global imports Zahrnout globální importy @@ -667,6 +672,11 @@ Lokalita + + Logging Roslyn Workspace structure... + Protokolování struktury pracovního prostoru Roslyn... + + Make '{0}' abstract Nastavit {0} jako abstraktní @@ -1362,6 +1372,11 @@ Výsledky sémantického vyhledávání + + Show Inheritance + Zobrazit dědičnost + + Show "Remove Unused References" command in Solution Explorer Zobrazit příkaz Odebrat nepoužívané odkazy v Průzkumníkovi řešení @@ -1697,6 +1712,11 @@ Hodnota, která se má vložit v místech volání + + Visual Studio + Visual Studio + + Visual Studio Settings Nastavení sady Visual Studio diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.de.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.de.xlf index 9dfab8b539d7..cc3b97727c05 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.de.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.de.xlf @@ -572,6 +572,11 @@ In anderen Operatoren + + Include Roslyn source-generated files + Roslyn-Quellcode-generierte Dateien einschließen + Used in the unified settings registration for real-time test discovery + Include global imports Globale Importe einschließen @@ -667,6 +672,11 @@ Speicherort + + Logging Roslyn Workspace structure... + Roslyn-Arbeitsbereichsstruktur wird protokolliert... + + Make '{0}' abstract "{0}" als abstrakt festlegen @@ -1362,6 +1372,11 @@ Semantische Suchergebnisse + + Show Inheritance + Vererbung anzeigen + + Show "Remove Unused References" command in Solution Explorer Befehl „Nicht verwendete Verweise entfernen“ in Projektmappen-Explorer anzeigen @@ -1697,6 +1712,11 @@ An Aufrufsites einzufügender Wert + + Visual Studio + Visual Studio + + Visual Studio Settings Visual Studio-Einstellungen diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.es.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.es.xlf index f90be834fe3b..4ce6b8d970a5 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.es.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.es.xlf @@ -572,6 +572,11 @@ En otros operadores + + Include Roslyn source-generated files + Incluir archivos generados por el origen de Roslyn + Used in the unified settings registration for real-time test discovery + Include global imports Incluir importaciones globales @@ -667,6 +672,11 @@ Ubicación + + Logging Roslyn Workspace structure... + Registrando estructura del área de trabajo de Roslyn... + + Make '{0}' abstract Convertir "{0}" en abstracto @@ -1362,6 +1372,11 @@ Resultados de búsqueda semántica + + Show Inheritance + Mostrar herencia + + Show "Remove Unused References" command in Solution Explorer Mostrar el comando "Quitar referencias sin usar" en Explorador de soluciones @@ -1697,6 +1712,11 @@ Valor que se va a insertar en los sitios de llamada + + Visual Studio + Visual Studio + + Visual Studio Settings Configuración de Visual Studio diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.fr.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.fr.xlf index 76d15601ea6b..fd288fc89e83 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.fr.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.fr.xlf @@ -572,6 +572,11 @@ Dans les autres opérateurs + + Include Roslyn source-generated files + Inclure les fichiers générés par Roslyn + Used in the unified settings registration for real-time test discovery + Include global imports Inclure les importations globales @@ -667,6 +672,11 @@ Emplacement + + Logging Roslyn Workspace structure... + Journalisation de la structure de l’espace de travail Roslyn... + + Make '{0}' abstract Rendre '{0}' abstrait @@ -1362,6 +1372,11 @@ Résultats de la recherche sémantique + + Show Inheritance + Afficher l’héritage + + Show "Remove Unused References" command in Solution Explorer Afficher la commande « Supprimer les références inutilisées » dans l’Explorateur de solutions @@ -1697,6 +1712,11 @@ Valeur à injecter sur les sites d'appel + + Visual Studio + Visual Studio + + Visual Studio Settings Paramètres de Visual Studio diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.it.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.it.xlf index 2cee6117e30d..a6a307a685f4 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.it.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.it.xlf @@ -572,6 +572,11 @@ In altri operatori + + Include Roslyn source-generated files + Includere file generati dall'origine Roslyn + Used in the unified settings registration for real-time test discovery + Include global imports Includi importazioni globali @@ -667,6 +672,11 @@ Posizione + + Logging Roslyn Workspace structure... + Registrazione della struttura dell'area di lavoro Roslyn in corso... + + Make '{0}' abstract Rendi astratto '{0}' @@ -1362,6 +1372,11 @@ Risultati della ricerca semantica + + Show Inheritance + Mostra ereditarietà + + Show "Remove Unused References" command in Solution Explorer Mostra il comando "Rimuovi riferimenti inutilizzati" in Esplora soluzioni @@ -1697,6 +1712,11 @@ Valore da inserire nei siti di chiamata + + Visual Studio + Visual Studio + + Visual Studio Settings Impostazioni di Visual Studio diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ja.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ja.xlf index 73da1d2e05b8..87fcbe8703ca 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ja.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ja.xlf @@ -572,6 +572,11 @@ その他の演算子内で + + Include Roslyn source-generated files + Roslyn ソース生成ファイルを含める + Used in the unified settings registration for real-time test discovery + Include global imports グローバル インポートを含める @@ -667,6 +672,11 @@ 場所 + + Logging Roslyn Workspace structure... + Roslyn ワークスペース構造をログに記録しています... + + Make '{0}' abstract '{0}' を抽象化する @@ -1362,6 +1372,11 @@ セマンティック検索結果 + + Show Inheritance + 継承を表示 + + Show "Remove Unused References" command in Solution Explorer ソリューション エクスプローラーで [未使用の参照を削除する] コマンドを表示する @@ -1697,6 +1712,11 @@ 呼び出しサイトで挿入する値 + + Visual Studio + Visual Studio + + Visual Studio Settings Visual Studio の設定 diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ko.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ko.xlf index 775a746a33f1..5a0951e3ebc8 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ko.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ko.xlf @@ -572,6 +572,11 @@ 기타 연산자 + + Include Roslyn source-generated files + Roslyn 소스 생성 파일 포함 + Used in the unified settings registration for real-time test discovery + Include global imports 전역 가져오기 포함 @@ -667,6 +672,11 @@ 위치 + + Logging Roslyn Workspace structure... + Roslyn 작업 영역 구조를 기록하는 중... + + Make '{0}' abstract '{0}'을(를) 추상으로 지정 @@ -1362,6 +1372,11 @@ 의미 체계 검색 결과 + + Show Inheritance + 상속 표시 + + Show "Remove Unused References" command in Solution Explorer 솔루션 탐색기에서 "사용하지 않는 참조 제거" 명령 표시 @@ -1697,6 +1712,11 @@ 호출 사이트에서 삽입할 값 + + Visual Studio + Visual Studio + + Visual Studio Settings Visual Studio 설정 diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pl.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pl.xlf index b16334c153c6..9737ab880f03 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pl.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pl.xlf @@ -572,6 +572,11 @@ W innych operatorach + + Include Roslyn source-generated files + Uwzględnij pliki wygenerowane przez źródło Roslyn + Used in the unified settings registration for real-time test discovery + Include global imports Uwzględnij globalne importy @@ -667,6 +672,11 @@ Lokalizacja + + Logging Roslyn Workspace structure... + Rejestrowanie struktury obszaru roboczego Roslyn... + + Make '{0}' abstract Ustaw element „{0}” jako abstrakcyjny @@ -1362,6 +1372,11 @@ Wyniki wyszukiwania semantycznego + + Show Inheritance + Pokaż dziedziczenie + + Show "Remove Unused References" command in Solution Explorer Pokaż polecenie „Usuń nieużywane odwołania” w Eksploratorze rozwiązań @@ -1697,6 +1712,11 @@ Wartość do iniekcji w lokalizacjach wywołania + + Visual Studio + Visual Studio + + Visual Studio Settings Ustawienia programu Visual Studio diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pt-BR.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pt-BR.xlf index a83195ce6fed..128a663b1453 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pt-BR.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.pt-BR.xlf @@ -572,6 +572,11 @@ Em outros operadores + + Include Roslyn source-generated files + Incluir arquivos gerados pelo Roslyn a partir do código-fonte + Used in the unified settings registration for real-time test discovery + Include global imports Incluir as importações globais @@ -667,6 +672,11 @@ Localização + + Logging Roslyn Workspace structure... + Estrutura do Workspace do Logging Roslyn... + + Make '{0}' abstract Fazer '{0}' abstrato @@ -1362,6 +1372,11 @@ Resultados da Pesquisa Semântica + + Show Inheritance + Mostrar Herança + + Show "Remove Unused References" command in Solution Explorer Mostrar o comando "Remover Referências Não Utilizadas" no Gerenciador de Soluções @@ -1697,6 +1712,11 @@ Valor a ser injetado nos sites de chamada + + Visual Studio + Visual Studio + + Visual Studio Settings Configurações do Visual Studio diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ru.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ru.xlf index 0e51d7f7f1e3..38249d5b2c29 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ru.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.ru.xlf @@ -572,6 +572,11 @@ В других операторах + + Include Roslyn source-generated files + Включить файлы, сгенерированные Roslyn + Used in the unified settings registration for real-time test discovery + Include global imports Включить глобальные директивы import @@ -667,6 +672,11 @@ Расположение + + Logging Roslyn Workspace structure... + Регистрация в журнале структуры рабочей области Roslyn... + + Make '{0}' abstract Сделать "{0}" абстрактным @@ -1362,6 +1372,11 @@ Результаты семантического поиска + + Show Inheritance + Показать наследование + + Show "Remove Unused References" command in Solution Explorer Показывать команду "Удалить неиспользуемые ссылки" в Обозревателе решений @@ -1697,6 +1712,11 @@ Значение, которое необходимо вставить во все точки вызова + + Visual Studio + Visual Studio + + Visual Studio Settings Параметры Visual Studio diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.tr.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.tr.xlf index 7bf5784dae00..de413fdf5b67 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.tr.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.tr.xlf @@ -572,6 +572,11 @@ Diğer işleçlerde + + Include Roslyn source-generated files + Roslyn kaynak tarafından oluşturulan dosyalarını dahil et + Used in the unified settings registration for real-time test discovery + Include global imports Genel içeri aktarmaları dahil et @@ -667,6 +672,11 @@ Konum + + Logging Roslyn Workspace structure... + Roslyn Çalışma Alanı yapısı günlüğe kaydediliyor... + + Make '{0}' abstract '{0}' değerini soyut yap @@ -1362,6 +1372,11 @@ Anlamsal Arama Sonuçları + + Show Inheritance + Devralmayı Göster + + Show "Remove Unused References" command in Solution Explorer Çözüm Gezgini'nde "Kullanılmayan Başvuruları Kaldır" komutunu göster @@ -1697,6 +1712,11 @@ Çağrı sitelerinde eklenecek değer + + Visual Studio + Visual Studio + + Visual Studio Settings Visual Studio Ayarları diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hans.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hans.xlf index a35f43415b51..f61a22f55a1c 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hans.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hans.xlf @@ -572,6 +572,11 @@ 在其他运算符中 + + Include Roslyn source-generated files + 包含 Roslyn 源生成的文件 + Used in the unified settings registration for real-time test discovery + Include global imports 包括全局导入 @@ -667,6 +672,11 @@ 位置 + + Logging Roslyn Workspace structure... + 正在记录 Roslyn 工作区结构... + + Make '{0}' abstract 将“{0}”设为抽象 @@ -1362,6 +1372,11 @@ 语义搜索结果 + + Show Inheritance + 显示继承 + + Show "Remove Unused References" command in Solution Explorer 在解决方案资源管理器中显示“删除未使用的引用”命令 @@ -1697,6 +1712,11 @@ 要在调用站点插入的值 + + Visual Studio + Visual Studio + + Visual Studio Settings Visual Studio 设置 diff --git a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hant.xlf b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hant.xlf index 9ba39963d821..155fde518a3d 100644 --- a/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hant.xlf +++ b/src/VisualStudio/Core/Def/xlf/ServicesVSResources.zh-Hant.xlf @@ -572,6 +572,11 @@ 其他運算子中 + + Include Roslyn source-generated files + 包含 Roslyn 來源產生的檔案 + Used in the unified settings registration for real-time test discovery + Include global imports 包含全域匯入 @@ -667,6 +672,11 @@ 位置 + + Logging Roslyn Workspace structure... + 正在記錄 Roslyn 工作區結構... + + Make '{0}' abstract 將 '{0}' 抽象化 @@ -1362,6 +1372,11 @@ 語意搜尋結果 + + Show Inheritance + 顯示繼承 + + Show "Remove Unused References" command in Solution Explorer 在方案總管中顯示「移除未使用的參考」命令 @@ -1697,6 +1712,11 @@ 要在呼叫位置插入的值 + + Visual Studio + Visual Studio + + Visual Studio Settings Visual Studio 設定 diff --git a/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs b/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs index 5dd1f96bbd49..1d1046855910 100644 --- a/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs +++ b/src/VisualStudio/Core/Impl/ProjectSystem/CPS/CPSProject_IWorkspaceProjectContext.cs @@ -21,6 +21,11 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.C internal sealed partial class CPSProject : IWorkspaceProjectContext { + /// + /// Flag to control if this has already been disposed. Not a boolean only so it can be used with Interlocked.CompareExchange. + /// + private volatile int _disposed = 0; + private readonly ProjectSystemProject _projectSystemProject; /// @@ -237,13 +242,31 @@ public void AddAdditionalFile(string filePath, bool isInCurrentContext = true) public void AddAdditionalFile(string filePath, IEnumerable folderNames, bool isInCurrentContext = true) => _projectSystemProject.AddAdditionalFile(filePath, folders: [.. folderNames]); + [Obsolete("Switch to using the DisposeAsync version of this API instead.")] public void Dispose() { + if (Interlocked.CompareExchange(ref _disposed, value: 1, comparand: 0) != 0) + { + return; + } + _projectCodeModel?.OnProjectClosed(); _projectSystemProjectOptionsProcessor?.Dispose(); _projectSystemProject.RemoveFromWorkspace(); } + public async ValueTask DisposeAsync() + { + if (Interlocked.CompareExchange(ref _disposed, value: 1, comparand: 0) != 0) + { + return; + } + + _projectCodeModel?.OnProjectClosed(); + _projectSystemProjectOptionsProcessor?.Dispose(); + await _projectSystemProject.RemoveFromWorkspaceAsync().ConfigureAwait(false); + } + public void AddAnalyzerReference(string referencePath) => _projectSystemProject.AddAnalyzerReference(referencePath); diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerNodeSetup.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerNodeSetup.cs index 4e554379874e..b7ebe9adfc9c 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerNodeSetup.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzerNodeSetup.cs @@ -4,10 +4,10 @@ using System; using System.ComponentModel.Composition; -using System.ComponentModel.Design; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.LanguageServices.Implementation.Utilities; using Microsoft.VisualStudio.Shell; namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; @@ -28,12 +28,10 @@ public AnalyzerNodeSetup( _analyzerCommandHandler = analyzerCommandHandler; } - public async Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken) + public async Task InitializeAsync(IAsyncServiceProvider serviceProvider, ThreadSafeMenuCommandService menuCommandService, CancellationToken cancellationToken) { await _analyzerTracker.RegisterAsync(serviceProvider, cancellationToken).ConfigureAwait(false); - await _analyzerCommandHandler.InitializeAsync( - await serviceProvider.GetServiceAsync(throwOnFailure: false, cancellationToken).ConfigureAwait(false), - cancellationToken).ConfigureAwait(false); + _analyzerCommandHandler.Initialize(menuCommandService); } public void Unregister() diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs index 278eb376d90c..54e492588cdf 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs @@ -96,33 +96,31 @@ public AnalyzersCommandHandler( /// /// Hook up the context menu handlers. /// - public async Task InitializeAsync(IMenuCommandService menuCommandService, CancellationToken cancellationToken) + public void Initialize(ThreadSafeMenuCommandService menuCommandService) { if (menuCommandService != null) { - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - // Analyzers folder context menu items - _addMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.AddAnalyzer, AddAnalyzerHandler); - _ = AddCommandHandler(menuCommandService, ID.RoslynCommands.OpenRuleSet, OpenRuleSetHandler); + _addMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.AddAnalyzer, AddAnalyzerHandler); + _ = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.OpenRuleSet, OpenRuleSetHandler); // Analyzer context menu items - _removeMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.RemoveAnalyzer, RemoveAnalyzerHandler); + _removeMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.RemoveAnalyzer, RemoveAnalyzerHandler); // Diagnostic context menu items - _setSeverityDefaultMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.SetSeverityDefault, SetSeverityHandler); - _setSeverityErrorMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.SetSeverityError, SetSeverityHandler); - _setSeverityWarningMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.SetSeverityWarning, SetSeverityHandler); - _setSeverityInfoMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.SetSeverityInfo, SetSeverityHandler); - _setSeverityHiddenMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.SetSeverityHidden, SetSeverityHandler); - _setSeverityNoneMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.SetSeverityNone, SetSeverityHandler); - _openHelpLinkMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.OpenDiagnosticHelpLink, OpenDiagnosticHelpLinkHandler); + _setSeverityDefaultMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.SetSeverityDefault, SetSeverityHandler); + _setSeverityErrorMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.SetSeverityError, SetSeverityHandler); + _setSeverityWarningMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.SetSeverityWarning, SetSeverityHandler); + _setSeverityInfoMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.SetSeverityInfo, SetSeverityHandler); + _setSeverityHiddenMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.SetSeverityHidden, SetSeverityHandler); + _setSeverityNoneMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.SetSeverityNone, SetSeverityHandler); + _openHelpLinkMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.OpenDiagnosticHelpLink, OpenDiagnosticHelpLinkHandler); // Other menu items - _projectAddMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.ProjectAddAnalyzer, AddAnalyzerHandler); - _projectContextAddMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.ProjectContextAddAnalyzer, AddAnalyzerHandler); - _referencesContextAddMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.ReferencesContextAddAnalyzer, AddAnalyzerHandler); - _setActiveRuleSetMenuItem = AddCommandHandler(menuCommandService, ID.RoslynCommands.SetActiveRuleSet, SetActiveRuleSetHandler); + _projectAddMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.ProjectAddAnalyzer, AddAnalyzerHandler); + _projectContextAddMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.ProjectContextAddAnalyzer, AddAnalyzerHandler); + _referencesContextAddMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.ReferencesContextAddAnalyzer, AddAnalyzerHandler); + _setActiveRuleSetMenuItem = menuCommandService.AddCommand(Guids.RoslynGroupId, ID.RoslynCommands.SetActiveRuleSet, SetActiveRuleSetHandler); UpdateOtherMenuItemsVisibility(); @@ -215,15 +213,6 @@ private void UpdateDiagnosticContextMenu() UpdateOpenHelpLinkMenuItemVisibility(); } - private static MenuCommand AddCommandHandler(IMenuCommandService menuCommandService, int roslynCommand, EventHandler handler) - { - var commandID = new CommandID(Guids.RoslynGroupId, roslynCommand); - var menuCommand = new MenuCommand(handler, commandID); - menuCommandService.AddCommand(menuCommand); - - return menuCommand; - } - private void SelectedHierarchyItemChangedHandler(object sender, EventArgs e) { UpdateOtherMenuItemsVisibility(); diff --git a/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs b/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs index c41c6825761d..050d32fb596b 100644 --- a/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs +++ b/src/VisualStudio/Core/Test.Next/Options/VisualStudioOptionStorageTests.cs @@ -239,6 +239,7 @@ public void OptionHasStorageIfNecessary(string configName) "dotnet_enable_automatic_restore", // VSCode only option for the VS Code project system; does not apply to VS "dotnet_enable_file_based_programs", // VSCode only option for the VS Code project system; does not apply to VS "dotnet_enable_file_based_programs_when_ambiguous", // VSCode only option for the VS Code project system; does not apply to VS + "dotnet_enable_automatic_discovery", // VSCode only option for the VS Code project system; does not apply to VS "dotnet_lsp_using_devkit", // VSCode internal only option. Does not need any UI. "dotnet_enable_references_code_lens", // VSCode only option. Does not apply to VS. "dotnet_enable_tests_code_lens", // VSCode only option. Does not apply to VS. diff --git a/src/VisualStudio/Core/Test.Next/Roslyn.VisualStudio.Next.UnitTests.csproj b/src/VisualStudio/Core/Test.Next/Roslyn.VisualStudio.Next.UnitTests.csproj index b0c15a4a0d9f..0968d3bf9240 100644 --- a/src/VisualStudio/Core/Test.Next/Roslyn.VisualStudio.Next.UnitTests.csproj +++ b/src/VisualStudio/Core/Test.Next/Roslyn.VisualStudio.Next.UnitTests.csproj @@ -65,6 +65,8 @@ + + diff --git a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs index 31d28f5eb529..f057c291a732 100644 --- a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs @@ -595,19 +595,17 @@ public async Task TestPartialProjectSync_GetSolutionLast() Assert.Equal(1, project1SyncedSolution.Projects.Count()); Assert.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); - // Syncing project 2 should end up with only p2 synced over. + // Syncing project 2 will have both projects since we reused the prior snapshot await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project2SyncedSolution.Projects.Count()); + Assert.Equal(2, project2SyncedSolution.Projects.Count()); - // then syncing the whole project should now copy both over. + // then syncing the whole solution should still contain both await solution.AppendAssetMapAsync(map, CancellationToken.None); var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - - Assert.Equal(solutionChecksum, await syncedFullSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); - Assert.Equal(2, syncedFullSolution.Projects.Count()); + Assert.Equal(2, project2SyncedSolution.Projects.Count()); } [Fact] @@ -669,23 +667,21 @@ public async Task TestPartialProjectSync_GetDependentProjects2() var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project3SyncedSolution.Projects.Count()); - // if we then sync just P2, we should still have only P2 in the synced cone + // if we then sync just P2, we will reuse our snapshot that contains P3 await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project2SyncedSolution.Projects.Count()); - AssertEx.Equal(project2.Name, project2SyncedSolution.Projects.Single().Name); + Assert.Equal(2, project2SyncedSolution.Projects.Count()); - // if we then sync just P1, we should only have it in its own cone. + // if we then sync just P1, we will reuse our prior snapshots with P2 and P3, so all projects will be present now await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project1SyncedSolution.Projects.Count()); - AssertEx.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); + Assert.Equal(3, project1SyncedSolution.Projects.Count()); } [Fact] - public async Task TestPartialProjectSync_GetDependentProjects3() + public async Task TestPartialProjectSync_GetDependentProjects2_ReverseOrder() { var code = @"class Test { void Method() { } }"; @@ -698,34 +694,33 @@ public async Task TestPartialProjectSync_GetDependentProjects3() var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); - solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) - .AddProjectReference(project2.Id, new(project1.Id)); + solution = project3.Solution.AddProjectReference(project3.Id, new(project3.Solution.Projects.Single(p => p.Name == "P2").Id)); var map = new Dictionary(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.SolutionServices); - // syncing project3 should since project2 and project1 as well because of the p2p ref - await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); - var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); + // syncing P1 should contain just P1 + await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); + var project3Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(3, project3SyncedSolution.Projects.Count()); + Assert.Equal(1, project3SyncedSolution.Projects.Count()); - // syncing project2 should only have it and project 1. + // if we then sync just P2, we will get both P1 and P2 -- P1, because we reused the snapshot await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(2, project2SyncedSolution.Projects.Count()); - // syncing project1 should only be itself - await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); - var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + // if we then sync just P3, we will reuse our prior snapshots with P1 and P2, so all projects will be present now + await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project1SyncedSolution.Projects.Count()); + Assert.Equal(3, project1SyncedSolution.Projects.Count()); } [Fact] - public async Task TestPartialProjectSync_GetDependentProjects4() + public async Task TestPartialProjectSync_GetDependentProjects3() { var code = @"class Test { void Method() { } }"; @@ -739,7 +734,7 @@ public async Task TestPartialProjectSync_GetDependentProjects4() var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) - .AddProjectReference(project3.Id, new(project1.Id)); + .AddProjectReference(project2.Id, new(project1.Id)); var map = new Dictionary(); var assetProvider = new AssetProvider( @@ -751,21 +746,21 @@ public async Task TestPartialProjectSync_GetDependentProjects4() var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); Assert.Equal(3, project3SyncedSolution.Projects.Count()); - // Syncing project2 should only have a cone with itself. + // syncing project2 will still have all projects since we're reusing the prior snapshot await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project2SyncedSolution.Projects.Count()); + Assert.Equal(3, project2SyncedSolution.Projects.Count()); - // Syncing project1 should only have a cone with itself. + // syncing project1 is the same: all projects should be present await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project1SyncedSolution.Projects.Count()); + Assert.Equal(3, project1SyncedSolution.Projects.Count()); } [Fact] - public async Task TestPartialProjectSync_Options1() + public async Task TestPartialProjectSync_GetDependentProjects3_ReverseOrder() { var code = @"class Test { void Method() { } }"; @@ -775,26 +770,73 @@ public async Task TestPartialProjectSync_Options1() var solution = workspace.CurrentSolution; var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.VisualBasic); + var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); + var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); - solution = project2.Solution; + solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) + .AddProjectReference(project2.Id, new(project1.Id)); var map = new Dictionary(); var assetProvider = new AssetProvider( Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.SolutionServices); - // Syncing over project1 should give us 1 set of options on the OOP side. + // syncing project1 will contain just P1 await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); - var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + var project3Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project3SyncedSolution.Projects.Count()); + + // syncing project2 will contain both + await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); + var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(2, project2SyncedSolution.Projects.Count()); + + // syncing project3 gives everything + await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project1SyncedSolution.Projects.Count()); - Assert.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); + Assert.Equal(3, project1SyncedSolution.Projects.Count()); + } + + [Fact] + public async Task TestPartialProjectSync_GetDependentProjects4() + { + var code = @"class Test { void Method() { } }"; + + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - // Syncing over project2 should also only be one set of options. + var solution = workspace.CurrentSolution; + + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); + var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); + + solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) + .AddProjectReference(project3.Id, new(project1.Id)); + + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.SolutionServices); + + // syncing project3 should since project2 and project1 as well because of the p2p ref + await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); + var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(3, project3SyncedSolution.Projects.Count()); + + // Syncing project2 will still contain all projects since we're reusing the prior snapshot await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project2SyncedSolution.Projects.Count()); + Assert.Equal(3, project2SyncedSolution.Projects.Count()); + + // Syncing project1 will still contain all projects since we're reusing the prior snapshot + await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(3, project1SyncedSolution.Projects.Count()); } [Fact] diff --git a/src/VisualStudio/Core/Test.Next/UnifiedSettings/UnifiedSettingsTests.cs b/src/VisualStudio/Core/Test.Next/UnifiedSettings/UnifiedSettingsTests.cs index cd888fc10978..52fe13ebc2c5 100644 --- a/src/VisualStudio/Core/Test.Next/UnifiedSettings/UnifiedSettingsTests.cs +++ b/src/VisualStudio/Core/Test.Next/UnifiedSettings/UnifiedSettingsTests.cs @@ -269,6 +269,14 @@ private static void VerifyProperties(JsonNode jsonDocument, string prefix, Immut #endregion + [Fact] + public async Task VerifyRoslynSettings() + { + using var registrationFileStream = typeof(UnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("Roslyn.VisualStudio.Next.UnitTests.roslynSettings.registration.json"); + using var streamReader = new StreamReader(registrationFileStream); + await VerifyTagAsync(streamReader.ReadToEnd(), "Roslyn.VisualStudio.Next.UnitTests.roslynPackageRegistration.pkgdef"); + } + #region Helpers private static async Task VerifyTagAsync(string registrationFile, string pkgdefFileName) diff --git a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb index b1395741e3c6..df340edacb32 100644 --- a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb +++ b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb @@ -5,7 +5,6 @@ Imports System.Collections.Immutable Imports System.Threading Imports Microsoft.CodeAnalysis -Imports Microsoft.CodeAnalysis.CodeActions Imports Microsoft.CodeAnalysis.CommonDiagnosticAnalyzers Imports Microsoft.CodeAnalysis.Diagnostics Imports Microsoft.CodeAnalysis.Editor.[Shared].Utilities @@ -16,6 +15,7 @@ Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.CodeAnalysis.Text Imports Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel Imports Microsoft.VisualStudio.LanguageServices.Implementation.TaskList +Imports Microsoft.VisualStudio.LanguageServices.TaskList Imports Microsoft.VisualStudio.LanguageServices.UnitTests.CodeModel.Mocks Imports Microsoft.VisualStudio.RpcContracts.DiagnosticManagement Imports Roslyn.Test.Utilities @@ -35,56 +35,67 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Private Shared ReadOnly s_projectGuid As Guid = Guid.NewGuid() - Public Async Function TestExternalDiagnostics_SupportedId() As Task + Public Async Function TestExternalDiagnostics_UnsupportedId() As Task Using workspace = EditorTestWorkspace.CreateCSharp(String.Empty, composition:=s_composition) - Dim waiter = workspace.GetService(Of AsynchronousOperationListenerProvider)().GetWaiter(FeatureAttribute.ErrorList) - Dim analyzer = New AnalyzerForErrorLogTest() + Dim listener = workspace.GetService(Of AsynchronousOperationListenerProvider)() + ' Create an analyzer and add it to the solution + Dim analyzer = New AnalyzerForErrorLogTest() Dim analyzerReference = New TestAnalyzerReferenceByLanguage( ImmutableDictionary(Of String, ImmutableArray(Of DiagnosticAnalyzer)).Empty.Add(LanguageNames.CSharp, ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer))) - workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences({analyzerReference})) - Dim threadingContext = workspace.ExportProvider.GetExport(Of IThreadingContext).Value - Dim vsWorkspace = workspace.ExportProvider.GetExportedValue(Of MockVisualStudioWorkspace)() vsWorkspace.SetWorkspace(workspace) - Using source = workspace.ExportProvider.GetExportedValue(Of ExternalErrorDiagnosticUpdateSource)() + ' Get the diagnostic cache through the mock VS workspace otherwise it will not be the correct instance + Dim diagnosticCache = vsWorkspace.Services.GetRequiredService(Of VisualStudioDiagnosticIdCache)() + + Using source = workspace.ExportProvider.GetExportedValue(Of ExternalErrorDiagnosticUpdateSource)() Dim project = workspace.CurrentSolution.Projects.First() + + ' Registering the project with the diagnostic cache will allow it to be populated (this mimics AbstractLegeacyProject) + diagnosticCache.RegisterProject(project.Id) + + ' Starting a build triggers the diagnostic id cache to refresh. Waiting on DiagnosticService ensures + ' that they are populated. source.OnSolutionBuildStarted() - Await waiter.ExpeditedWaitAsync() + Await listener.WaitAllAsync(workspace, {FeatureAttribute.DiagnosticService, FeatureAttribute.ErrorList}) - Assert.True(Await source.IsSupportedDiagnosticIdAsync(project.Id, "ID1", CancellationToken.None)) - Assert.False(Await source.IsSupportedDiagnosticIdAsync(project.Id, "CA1002", CancellationToken.None)) + Assert.False(source.IsUnsupportedDiagnosticId(project.Id, "ID1")) + Assert.True(source.IsUnsupportedDiagnosticId(project.Id, "CA1002")) End Using End Using End Function - Public Async Function TestExternalDiagnostics_SupportedIdFalseIfBuildNotStarted() As Task + Public Sub TestExternalDiagnostics_UnsupportedIdTrueIfBuildNotStarted() Using workspace = EditorTestWorkspace.CreateCSharp(String.Empty, composition:=s_composition) - Dim waiter = workspace.GetService(Of AsynchronousOperationListenerProvider)().GetWaiter(FeatureAttribute.ErrorList) + ' Create an analyzer and add it to the solution Dim analyzer = New AnalyzerForErrorLogTest() - Dim analyzerReference = New TestAnalyzerReferenceByLanguage( ImmutableDictionary(Of String, ImmutableArray(Of DiagnosticAnalyzer)).Empty.Add(LanguageNames.CSharp, ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer))) - workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences({analyzerReference})) - Dim threadingContext = workspace.ExportProvider.GetExport(Of IThreadingContext).Value - Dim vsWorkspace = workspace.ExportProvider.GetExportedValue(Of MockVisualStudioWorkspace)() vsWorkspace.SetWorkspace(workspace) - Using source = workspace.ExportProvider.GetExportedValue(Of ExternalErrorDiagnosticUpdateSource)() + ' Get the diagnostic cache through the mock VS workspace otherwise it will not be the correct instance + Dim diagnosticCache = vsWorkspace.Services.GetRequiredService(Of VisualStudioDiagnosticIdCache)() + + Using source = workspace.ExportProvider.GetExportedValue(Of ExternalErrorDiagnosticUpdateSource)() Dim project = workspace.CurrentSolution.Projects.First() - Assert.False(Await source.IsSupportedDiagnosticIdAsync(project.Id, "ID1", CancellationToken.None)) - Assert.False(Await source.IsSupportedDiagnosticIdAsync(project.Id, "CA1002", CancellationToken.None)) + ' Registering the project with the diagnostic cache will allow it to be populated (this mimics AbstractLegeacyProject) + diagnosticCache.RegisterProject(project.Id) + + ' By not starting a build the cache will not populate and we cannot say a diagnostic is unsupported. + + Assert.True(source.IsUnsupportedDiagnosticId(project.Id, "ID1")) + Assert.True(source.IsUnsupportedDiagnosticId(project.Id, "CA1002")) End Using End Using - End Function + End Sub Public Async Function TestExternalDiagnosticsReported() As Task diff --git a/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticIdCacheTests.vb b/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticIdCacheTests.vb new file mode 100644 index 000000000000..17f6d555c2ca --- /dev/null +++ b/src/VisualStudio/Core/Test/Diagnostics/VisualStudioDiagnosticIdCacheTests.vb @@ -0,0 +1,199 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports System.Collections.Immutable +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Diagnostics +Imports Microsoft.CodeAnalysis.Serialization +Imports Microsoft.CodeAnalysis.Shared.TestHooks +Imports Microsoft.CodeAnalysis.Test.Utilities +Imports Microsoft.VisualStudio.LanguageServices.TaskList +Imports Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Framework +Imports Roslyn.Test.Utilities + +Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics + + <[UseExportProvider]> + Public Class VisualStudioDiagnosticIdCacheTests + Private Shared ReadOnly s_composition As TestComposition = VisualStudioTestCompositions.LanguageServices + + + Public Async Function LegacyProject_RegistersProjectWithCache() As Task + Using environment = New TestEnvironment() + Dim workspace = environment.Workspace + Dim diagnosticIdCache = workspace.Services.GetRequiredService(Of VisualStudioDiagnosticIdCache)() + Dim listenerProvider = environment.ExportProvider.GetExportedValue(Of AsynchronousOperationListenerProvider)() + + ' There should be no registered projects in an empty workspace. + Assert.Equal(0, diagnosticIdCache.GetTestAccessor().RegisteredProjectCount) + + ' Create a legacy project. + Dim csharpProject = CSharpHelpers.CreateCSharpProject(environment, "Test") + Dim project = workspace.CurrentSolution.Projects.Single() + + ' The project should have been registered with the cache but it should be empty until diagnostics are fetched. + Assert.Equal(1, diagnosticIdCache.GetTestAccessor().RegisteredProjectCount) + Assert.False(diagnosticIdCache.TryGetDiagnosticIds(project.Id, Nothing)) + + ' Asking the cache to refresh and waiting for DiagnosticService to process should populate the cache. + diagnosticIdCache.Refresh() + Await listenerProvider.WaitAllAsync(workspace, {FeatureAttribute.DiagnosticService}) + + ' Cache should now be populated and contain the legacy project id. + Assert.True(diagnosticIdCache.TryGetDiagnosticIds(project.Id, Nothing)) + End Using + End Function + + + Public Async Function TestDiagnosticDescriptorCache_Populates() As Task + Using workspace = EditorTestWorkspace.CreateCSharp("class A { }", composition:=s_composition) + Dim diagnosticIdCache = workspace.Services.GetRequiredService(Of VisualStudioDiagnosticIdCache)() + Dim listenerProvider = workspace.GetService(Of AsynchronousOperationListenerProvider)() + + ' Create an in memory analyzer. + Dim analyzer = New DescriptorOnlyAnalyzer("CACHE001") + Dim analyzerReference = New AnalyzerImageReference(ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer)) + SerializerService.TestAccessor.AddAnalyzerImageReference(analyzerReference) + + ' Add the analyzer reference to our project. + Dim project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(analyzerReference) + Assert.True(workspace.TryApplyChanges(project.Solution)) + project = workspace.CurrentSolution.Projects.Single() + + ' The cache should not contain the project until it is registered. + Assert.False(diagnosticIdCache.TryGetDiagnosticIds(project.Id, Nothing)) + + ' Register the project with cache (this mimics what LegacyProjects do automatically) and request + ' the cache to refresh (this mimics what ExternalErrorDiagnosticUpdateSource does when a build is + ' started). Waiting for the DiagnosticService to process should populate the cache. + diagnosticIdCache.RegisterProject(project.Id) + diagnosticIdCache.Refresh() + Await listenerProvider.WaitAllAsync(workspace, {FeatureAttribute.DiagnosticService}) + + ' Cache should be populated and contain the diagnostic id from our analyzer. + Dim diagnosticIds As ImmutableHashSet(Of String) = Nothing + Assert.True(diagnosticIdCache.TryGetDiagnosticIds(project.Id, diagnosticIds)) + Assert.Contains("CACHE001", diagnosticIds) + End Using + End Function + + + Public Async Function TestDiagnosticDescriptorCache_RefreshesOnAnalyzerReferenceChange() As Task + Using workspace = EditorTestWorkspace.CreateCSharp("class A { }", composition:=s_composition) + Dim diagnosticIdCache = workspace.Services.GetRequiredService(Of VisualStudioDiagnosticIdCache)() + Dim listenerProvider = workspace.GetService(Of AsynchronousOperationListenerProvider)() + + ' Create an in memory analyzer. + Dim analyzer1 = New DescriptorOnlyAnalyzer("CACHE001") + Dim analyzerReference1 = New AnalyzerImageReference(ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer1)) + SerializerService.TestAccessor.AddAnalyzerImageReference(analyzerReference1) + + ' Add the analyze reference to our project. + Dim project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(analyzerReference1) + Assert.True(workspace.TryApplyChanges(project.Solution)) + project = workspace.CurrentSolution.Projects.Single() + + ' Register the project with cache (this mimics what LegacyProjects do automatically) and request + ' the cache to refresh (this mimics what ExternalErrorDiagnosticUpdateSource does when a build is + ' started). Waiting for the DiagnosticService to process should populate the cache. + diagnosticIdCache.RegisterProject(project.Id) + diagnosticIdCache.Refresh() + Await listenerProvider.WaitAllAsync(workspace, {FeatureAttribute.DiagnosticService}) + + ' Cache should be populated and contain the diagnostic id from our analyzer. + Dim initialDiagnosticIds As ImmutableHashSet(Of String) = Nothing + Assert.True(diagnosticIdCache.TryGetDiagnosticIds(project.Id, initialDiagnosticIds)) + Assert.Contains("CACHE001", initialDiagnosticIds) + + ' Create an in memory analyzer with a new diagnostic id. + Dim analyzer2 = New DescriptorOnlyAnalyzer("CACHE002") + Dim analyzerReference2 = New AnalyzerImageReference(ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer2)) + SerializerService.TestAccessor.AddAnalyzerImageReference(analyzerReference2) + + ' Replace the analyzer references with only the new one. + project = workspace.CurrentSolution.Projects.Single().WithAnalyzerReferences({analyzerReference2}) + Assert.True(workspace.TryApplyChanges(project.Solution)) + project = workspace.CurrentSolution.Projects.Single() + + ' Wait for the workspace change event to propagate to the cache. + Await listenerProvider.WaitAllAsync(workspace, {FeatureAttribute.Workspace}) + + ' Request the cache to refresh and wait for the DiagnosticService to process should populate the cache. + diagnosticIdCache.Refresh() + Await listenerProvider.WaitAllAsync(workspace, {FeatureAttribute.DiagnosticService}) + + ' The cache should now contain the new diagnostic id but not the old one. + Dim refreshedDiagnosticIds As ImmutableHashSet(Of String) = Nothing + Assert.True(diagnosticIdCache.TryGetDiagnosticIds(project.Id, refreshedDiagnosticIds)) + Assert.Contains("CACHE002", refreshedDiagnosticIds) + Assert.DoesNotContain("CACHE001", refreshedDiagnosticIds) + End Using + End Function + + + Public Async Function TestDiagnosticDescriptorCache_RemovesDeletedProjects() As Task + Using workspace = EditorTestWorkspace.CreateCSharp("class A { }", composition:=s_composition) + Dim diagnosticIdCache = workspace.Services.GetRequiredService(Of VisualStudioDiagnosticIdCache)() + Dim listenerProvider = workspace.GetService(Of AsynchronousOperationListenerProvider)() + + ' Create an in memory analyzer. + Dim analyzer = New DescriptorOnlyAnalyzer("CACHE001") + Dim analyzerReference = New AnalyzerImageReference(ImmutableArray.Create(Of DiagnosticAnalyzer)(analyzer)) + SerializerService.TestAccessor.AddAnalyzerImageReference(analyzerReference) + + ' Add the analyze reference to our project. + Dim project = workspace.CurrentSolution.Projects.Single().AddAnalyzerReference(analyzerReference) + Assert.True(workspace.TryApplyChanges(project.Solution)) + project = workspace.CurrentSolution.Projects.Single() + + ' Register the project with cache (this mimics what LegacyProjects do automatically) and request + ' the cache to refresh (this mimics what ExternalErrorDiagnosticUpdateSource does when a build is + ' started). Waiting for the DiagnosticService to process should populate the cache. + diagnosticIdCache.RegisterProject(project.Id) + diagnosticIdCache.Refresh() + Await listenerProvider.WaitAllAsync(workspace, {FeatureAttribute.DiagnosticService}) + + ' The cache should now contain the project and have one registered project. + Assert.True(diagnosticIdCache.TryGetDiagnosticIds(project.Id, Nothing)) + Assert.Equal(1, diagnosticIdCache.GetTestAccessor().RegisteredProjectCount) + + ' Remove the project from the workspace. + Dim newSolution = workspace.CurrentSolution.RemoveProject(project.Id) + Assert.True(workspace.TryApplyChanges(newSolution)) + + ' Wait for the workspace change event to propagate to the cache. + Await listenerProvider.WaitAllAsync(workspace, {FeatureAttribute.Workspace}) + + ' Request the cache to refresh and wait for the DiagnosticService to process should populate the cache. + diagnosticIdCache.Refresh() + Await listenerProvider.WaitAllAsync(workspace, {FeatureAttribute.DiagnosticService}) + + ' Ensure the cache no longer contains the project and it has been unregistered. + Assert.False(diagnosticIdCache.TryGetDiagnosticIds(project.Id, Nothing)) + Assert.Equal(0, diagnosticIdCache.GetTestAccessor().RegisteredProjectCount) + End Using + End Function + + Private NotInheritable Class DescriptorOnlyAnalyzer + Inherits DiagnosticAnalyzer + + Private ReadOnly _descriptor As DiagnosticDescriptor + + Public Sub New(id As String) + _descriptor = New DiagnosticDescriptor(id, "title", "message", "category", DiagnosticSeverity.Warning, isEnabledByDefault:=True) + End Sub + + Public Overrides ReadOnly Property SupportedDiagnostics As ImmutableArray(Of DiagnosticDescriptor) + Get + Return ImmutableArray.Create(_descriptor) + End Get + End Property + + Public Overrides Sub Initialize(context As AnalysisContext) + End Sub + End Class + + End Class + +End Namespace diff --git a/src/VisualStudio/Core/Test/ObjectBrowser/AbstractObjectBrowserTests.vb b/src/VisualStudio/Core/Test/ObjectBrowser/AbstractObjectBrowserTests.vb index fca0bed258cd..248e3aa452a9 100644 --- a/src/VisualStudio/Core/Test/ObjectBrowser/AbstractObjectBrowserTests.vb +++ b/src/VisualStudio/Core/Test/ObjectBrowser/AbstractObjectBrowserTests.vb @@ -45,7 +45,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ObjectBrowser Dim mockComponentModel = New MockComponentModel(workspace.ExportProvider) mockComponentModel.ProvideService(Of VisualStudioWorkspace)(vsWorkspace) Dim mockServiceProvider = workspace.ExportProvider.GetExportedValue(Of MockServiceProvider) - Dim libraryManager = CreateLibraryManager(mockServiceProvider, mockComponentModel, vsWorkspace) + Dim libraryManager = CreateLibraryManager(mockServiceProvider, mockComponentModel) result = New TestState(workspace, vsWorkspace, libraryManager) Finally @@ -57,7 +57,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ObjectBrowser Return result End Function - Friend MustOverride Function CreateLibraryManager(serviceProvider As IServiceProvider, componentModel As IComponentModel, workspace As VisualStudioWorkspace) As AbstractObjectBrowserLibraryManager + Friend MustOverride Function CreateLibraryManager(serviceProvider As IServiceProvider, componentModel As IComponentModel) As AbstractObjectBrowserLibraryManager Friend Function ProjectNode(name As String) As NavInfoNodeDescriptor Return New NavInfoNodeDescriptor With {.Kind = ObjectListKind.Projects, .Name = name} diff --git a/src/VisualStudio/Core/Test/ObjectBrowser/CSharp/ObjectBrowerTests.vb b/src/VisualStudio/Core/Test/ObjectBrowser/CSharp/ObjectBrowerTests.vb index 2c18867314fd..e8d481a6bdbd 100644 --- a/src/VisualStudio/Core/Test/ObjectBrowser/CSharp/ObjectBrowerTests.vb +++ b/src/VisualStudio/Core/Test/ObjectBrowser/CSharp/ObjectBrowerTests.vb @@ -21,8 +21,8 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ObjectBrowser.CSharp End Get End Property - Friend Overrides Function CreateLibraryManager(serviceProvider As IServiceProvider, componentModel As IComponentModel, workspace As VisualStudioWorkspace) As AbstractObjectBrowserLibraryManager - Return New ObjectBrowserLibraryManager(serviceProvider, componentModel, workspace) + Friend Overrides Function CreateLibraryManager(serviceProvider As IServiceProvider, componentModel As IComponentModel) As AbstractObjectBrowserLibraryManager + Return New ObjectBrowserLibraryManager(serviceProvider, componentModel) End Function diff --git a/src/VisualStudio/Core/Test/ObjectBrowser/VisualBasic/ObjectBrowerTests.vb b/src/VisualStudio/Core/Test/ObjectBrowser/VisualBasic/ObjectBrowerTests.vb index 2374283e683a..d989f60e4d29 100644 --- a/src/VisualStudio/Core/Test/ObjectBrowser/VisualBasic/ObjectBrowerTests.vb +++ b/src/VisualStudio/Core/Test/ObjectBrowser/VisualBasic/ObjectBrowerTests.vb @@ -21,8 +21,8 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ObjectBrowser.Visual End Get End Property - Friend Overrides Function CreateLibraryManager(serviceProvider As IServiceProvider, componentModel As IComponentModel, workspace As VisualStudioWorkspace) As AbstractObjectBrowserLibraryManager - Return New ObjectBrowserLibraryManager(serviceProvider, componentModel, workspace) + Friend Overrides Function CreateLibraryManager(serviceProvider As IServiceProvider, componentModel As IComponentModel) As AbstractObjectBrowserLibraryManager + Return New ObjectBrowserLibraryManager(serviceProvider, componentModel) End Function diff --git a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/MetadataToProjectReferenceConversionTests.vb b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/MetadataToProjectReferenceConversionTests.vb index 6200fa2f7416..c02147e75fe3 100644 --- a/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/MetadataToProjectReferenceConversionTests.vb +++ b/src/VisualStudio/Core/Test/ProjectSystemShim/VisualStudioProjectTests/MetadataToProjectReferenceConversionTests.vb @@ -6,8 +6,11 @@ Imports System.Collections.Immutable Imports System.Threading Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Test.Utilities +Imports Microsoft.CodeAnalysis.Workspaces.ProjectSystem Imports Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Framework +Imports Microsoft.VisualStudio.Threading Imports Roslyn.Test.Utilities +Imports Roslyn.Utilities Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim <[UseExportProvider]> @@ -369,5 +372,178 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim Assert.Single(environment.Workspace.CurrentSolution.Projects.Single().MetadataReferences) End Using End Function + + + Public Async Function RemovingProjectReferencingItselfDoesNotBreakIfASecondProjectExists() As Task + Using environment = New TestEnvironment() + Const ReferencePath = "C:\project.dll" + + Dim project1 = Await environment.ProjectFactory.CreateAndAddToWorkspaceAsync("project1", LanguageNames.CSharp, CancellationToken.None) + project1.OutputFilePath = ReferencePath + project1.AddMetadataReference(ReferencePath, MetadataReferenceProperties.Assembly) + + Dim project2 = Await environment.ProjectFactory.CreateAndAddToWorkspaceAsync("project2", LanguageNames.CSharp, CancellationToken.None) + project2.OutputFilePath = ReferencePath + + ' The removal of project one might accidentally try to convert the metadata reference to a project reference to project 2 + project1.RemoveFromWorkspace() + End Using + End Function + + + Public Async Function ProjectReferenceInformation_UnmanagedProject() As Task + Using environment = New TestEnvironment() + ' Ensure that ProjectSystemProjectFactory does not track ProjectReferenceInformation for projects it didn't add ("unmanaged projects"). + ' Doing so can cause ProjectSystemProjectFactory's internal invariants to be violated when the unmanaged project is removed. + Dim unmanagedProjectId As ProjectId = Nothing + environment.Workspace.SetCurrentSolution( + Function(solution) + Dim project = solution.AddProject("UnmanagedProject", "UnmanagedProject", LanguageNames.CSharp) + unmanagedProjectId = project.Id + Return project.Solution + End Function, WorkspaceChangeKind.ProjectAdded) + + Dim project1 = Await environment.ProjectFactory.CreateAndAddToWorkspaceAsync("project1", LanguageNames.CSharp, CancellationToken.None) + project1.OutputFilePath = "C:\out1.dll" + ' Changing the output path again will cause us to search the workspace for projects which had a ProjectReference to 'project1', based on its previous OutputPath, + ' and change them back to MetadataReferences if applicable. This can cause us to inadvertently hold onto information for unmanaged projects. + project1.OutputFilePath = "C:\out2.dll" + + ' Remove the unmanaged project + environment.Workspace.SetCurrentSolution( + Function(solution) solution.RemoveProject(unmanagedProjectId), WorkspaceChangeKind.ProjectAdded) + + project1.RemoveFromWorkspace() + End Using + End Function + + ''' + ''' Stress test that creates multiple threads which randomly create projects, add/remove + ''' metadata references (including shared references and project output references), + ''' and remove projects. This is used to test out bugs that might not be thought of in the regular + ''' set of tests. + ''' + + Public Async Function StressConcurrentProjectAndReferenceOperations() As Task + Using environment = New TestEnvironment() + Const ThreadCount = 8 + Const OperationsPerThread = 2000 + Const MaxProjectsPerThread = 5 + + ' Some normal reference paths that multiple projects may reference + Dim regularReferencePaths = Enumerable.Range(0, 5).Select(Function(i) $"Z:\Regular\Regular{i}.dll").ToArray() + + ' Output paths assigned to projects so that metadata references to these + ' paths are converted to project references. Paths will be chosen at random + ' from this set, so some projects may have duplicate output paths. + Dim outputPaths = Enumerable.Range(0, 20).Select(Function(i) $"Z:\Outputs\Project{i}.dll").ToArray() + + Dim barrier = New Barrier(ThreadCount) + + Dim tasks = Enumerable.Range(0, ThreadCount).Select( + Function(threadIndex) + Return Task.Run( + Sub() + ' We'll create a random generator per thread, using a stable index, so the behavior if a single + ' thread would be deterministic + Dim random = New Random(threadIndex) + Dim projects = New List(Of ProjectAndMetadataReferences)() + + ' Wait for all threads to start + barrier.SignalAndWait() + + For op = 1 To OperationsPerThread + Dim roll = random.Next(100) + + If roll < 25 OrElse projects.Count = 0 Then + ' Create a new project; first see if we already have enough + If projects.Count >= MaxProjectsPerThread Then + ' Remove the oldest + projects(0).Project.RemoveFromWorkspace() + projects.RemoveAt(0) + End If + + Dim name = $"project_t{threadIndex}_{op}" + Dim project = environment.ProjectFactory.CreateAndAddToWorkspaceAsync( + name, LanguageNames.CSharp, CancellationToken.None).Result + + ' Optionally assign an output path from the pool so other + ' threads' metadata references to this path convert to + ' project references. + If random.Next(100) < 50 Then + project.OutputFilePath = outputPaths(random.Next(outputPaths.Length)) + End If + + projects.Add(New ProjectAndMetadataReferences(project)) + + ElseIf roll < 55 Then + ' Add a metadata reference to a regular reference + Dim target = projects(random.Next(projects.Count)) + Dim refPath = regularReferencePaths(random.Next(regularReferencePaths.Length)) + If Not target.MetadataReferences.Contains(refPath) Then + target.Project.AddMetadataReference(refPath, MetadataReferenceProperties.Assembly) + target.MetadataReferences.Add(refPath) + End If + + ElseIf roll < 70 Then + ' Add a metadata reference to some project's output path + Dim target = projects(random.Next(projects.Count)) + Dim refPath = outputPaths(random.Next(outputPaths.Length)) + If Not target.MetadataReferences.Contains(refPath) Then + target.Project.AddMetadataReference(refPath, MetadataReferenceProperties.Assembly) + target.MetadataReferences.Add(refPath) + End If + + ElseIf roll < 85 Then + ' Remove a metadata reference + Dim target = projects(random.Next(projects.Count)) + If target.MetadataReferences.Count > 0 Then + Dim index = random.Next(target.MetadataReferences.Count) + Dim refPath = target.MetadataReferences(index) + target.Project.RemoveMetadataReference(refPath, MetadataReferenceProperties.Assembly) + target.MetadataReferences.RemoveAt(index) + End If + + ElseIf roll < 93 Then + ' Change a project's output path + Dim target = projects(random.Next(projects.Count)) + If random.Next(100) < 70 Then + Dim newPath = outputPaths(random.Next(outputPaths.Length)) + target.Project.OutputFilePath = newPath + Else + target.Project.OutputFilePath = Nothing + End If + + Else + ' Remove a project + If projects.Count > 1 Then + Dim index = random.Next(projects.Count) + projects(index).Project.RemoveFromWorkspace() + projects.RemoveAt(index) + End If + End If + Next + + ' Clean up remaining projects on this thread. + For Each p In projects + p.Project.RemoveFromWorkspace() + Next + End Sub) + End Function).ToArray() + + ' Awaiting the WhenAll might ignore the other exceptions; assert all of them to make it easier to see all the failures + Await Task.WhenAll(tasks).NoThrowAwaitable() + Assert.Empty(tasks.Select(Function(t) t.Exception?.InnerException).WhereNotNull()) + End Using + End Function + + Private NotInheritable Class ProjectAndMetadataReferences + Public ReadOnly Project As ProjectSystemProject + Public ReadOnly MetadataReferences As New List(Of String)() + + Public Sub New(project As ProjectSystemProject) + Me.Project = project + End Sub + End Class End Class End Namespace diff --git a/src/VisualStudio/Core/Test/SymbolSearch/SymbolSearchUpdateEngineTests.vb b/src/VisualStudio/Core/Test/SymbolSearch/SymbolSearchUpdateEngineTests.vb index 55751b94d400..149ba450b642 100644 --- a/src/VisualStudio/Core/Test/SymbolSearch/SymbolSearchUpdateEngineTests.vb +++ b/src/VisualStudio/Core/Test/SymbolSearch/SymbolSearchUpdateEngineTests.vb @@ -255,7 +255,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SymbolSearch Dim factoryMock = New Mock(Of IDatabaseFactoryService)(MockBehavior.Strict) ' Simulate Elfie throwing when trying to make a database from the contents of that response - factoryMock.Setup(Function(f) f.CreateDatabaseFromBytes(It.IsAny(Of Byte()), It.IsAny(Of Boolean))). + factoryMock.Setup(Function(f) f.CreateDatabaseFromStream(It.IsAny(Of Stream), It.IsAny(Of Boolean))). Throws(New NotImplementedException()) ' Because the parsing failed we will expect to call into the 'UpdateFailedDelay' to @@ -300,7 +300,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SymbolSearch ' Successfully create a database from that response. Dim factoryMock = New Mock(Of IDatabaseFactoryService)(MockBehavior.Strict) - factoryMock.Setup(Function(f) f.CreateDatabaseFromBytes(It.IsAny(Of Byte()), It.IsAny(Of Boolean))). + factoryMock.Setup(Function(f) f.CreateDatabaseFromStream(It.IsAny(Of Stream), It.IsAny(Of Boolean))). Returns(New AddReferenceDatabase()) ' Expect that we'll write the database to disk successfully. @@ -349,7 +349,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SymbolSearch ' Create a database from the client response. Dim factoryMock = New Mock(Of IDatabaseFactoryService)(MockBehavior.Strict) - factoryMock.Setup(Function(f) f.CreateDatabaseFromBytes(It.IsAny(Of Byte()), It.IsAny(Of Boolean))). + factoryMock.Setup(Function(f) f.CreateDatabaseFromStream(It.IsAny(Of Stream), It.IsAny(Of Boolean))). Returns(New AddReferenceDatabase()) Dim delayMock = New Mock(Of IDelayService)(MockBehavior.Strict) @@ -399,10 +399,11 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SymbolSearch ' Simulate the database being there. ioMock.Setup(Function(s) s.Exists(It.IsAny(Of FileSystemInfo))).Returns(True) ioMock.Setup(Function(s) s.ReadAllBytes(It.IsAny(Of String))).Returns({}) + ioMock.Setup(Function(s) s.OpenRead(It.IsAny(Of String))).Returns(New MemoryStream()) ' We'll successfully read in the local database. Dim databaseFactoryMock = New Mock(Of IDatabaseFactoryService)(MockBehavior.Strict) - databaseFactoryMock.Setup(Function(f) f.CreateDatabaseFromBytes(It.IsAny(Of Byte()), It.IsAny(Of Boolean))). + databaseFactoryMock.Setup(Function(f) f.CreateDatabaseFromStream(It.IsAny(Of Stream), It.IsAny(Of Boolean))). Returns(New AddReferenceDatabase()) ' Create a client that will return a patch that says things are up to date. @@ -443,11 +444,12 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SymbolSearch ' Simulate the database being there. ioMock.Setup(Function(s) s.Exists(It.IsAny(Of FileSystemInfo))).Returns(True) ioMock.Setup(Function(s) s.ReadAllBytes(It.IsAny(Of String))).Returns({}) + ioMock.Setup(Function(s) s.OpenRead(It.IsAny(Of String))).Returns(New MemoryStream()) ioMock.Setup(Sub(s) s.Delete(It.IsAny(Of FileInfo))) ' We'll successfully read in the local database. Dim databaseFactoryMock = New Mock(Of IDatabaseFactoryService)(MockBehavior.Strict) - databaseFactoryMock.Setup(Function(f) f.CreateDatabaseFromBytes(It.IsAny(Of Byte()), It.IsAny(Of Boolean))). + databaseFactoryMock.Setup(Function(f) f.CreateDatabaseFromStream(It.IsAny(Of Stream), It.IsAny(Of Boolean))). Returns(New AddReferenceDatabase()) ' Create a client that will return a patch that says things are too old. @@ -497,11 +499,12 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SymbolSearch ' Simulate the database being there. ioMock.Setup(Function(s) s.Exists(It.IsAny(Of FileSystemInfo))).Returns(True) ioMock.Setup(Function(s) s.ReadAllBytes(It.IsAny(Of String))).Returns({}) + ioMock.Setup(Function(s) s.OpenRead(It.IsAny(Of String))).Returns(New MemoryStream()) ioMock.Setup(Sub(s) s.Delete(It.IsAny(Of FileInfo))) ' We'll successfully read in the local database. Dim databaseFactoryMock = New Mock(Of IDatabaseFactoryService)(MockBehavior.Strict) - databaseFactoryMock.Setup(Function(f) f.CreateDatabaseFromBytes(It.IsAny(Of Byte()), It.IsAny(Of Boolean))). + databaseFactoryMock.Setup(Function(f) f.CreateDatabaseFromStream(It.IsAny(Of Stream), It.IsAny(Of Boolean))). Returns(New AddReferenceDatabase()) ' Create a client that will return a patch with contents. @@ -557,11 +560,12 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.SymbolSearch ' Simulate the database being there. ioMock.Setup(Function(s) s.Exists(It.IsAny(Of FileSystemInfo))).Returns(True) ioMock.Setup(Function(s) s.ReadAllBytes(It.IsAny(Of String))).Returns({}) + ioMock.Setup(Function(s) s.OpenRead(It.IsAny(Of String))).Returns(New MemoryStream()) ioMock.Setup(Sub(s) s.Delete(It.IsAny(Of FileInfo))) ' We'll successfully read in the local database. Dim databaseFactoryMock = New Mock(Of IDatabaseFactoryService)(MockBehavior.Strict) - databaseFactoryMock.Setup(Function(f) f.CreateDatabaseFromBytes(It.IsAny(Of Byte()), It.IsAny(Of Boolean))). + databaseFactoryMock.Setup(Function(f) f.CreateDatabaseFromStream(It.IsAny(Of Stream), It.IsAny(Of Boolean))). Returns(New AddReferenceDatabase()) ' Create a client that will return a patch with contents. diff --git a/src/VisualStudio/DevKit/Impl/EditAndContinue/LspSolutionSnapshotProvider.cs b/src/VisualStudio/DevKit/Impl/EditAndContinue/LspSolutionSnapshotProvider.cs new file mode 100644 index 000000000000..61b0de9592bf --- /dev/null +++ b/src/VisualStudio/DevKit/Impl/EditAndContinue/LspSolutionSnapshotProvider.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.BrokeredServices; +using Microsoft.CodeAnalysis.Contracts.Client; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[Shared] +[Export(typeof(ISolutionSnapshotProvider))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class LspSolutionSnapshotProvider( + IServiceBrokerProvider serviceBrokerProvider, + SolutionSnapshotRegistry solutionSnapshotRegistry) + : BrokeredServiceProxy(serviceBrokerProvider.ServiceBroker, BrokeredServiceDescriptors.SolutionSnapshotProvider), + ISolutionSnapshotProvider, + IDisposable +{ + public void Dispose() + { + solutionSnapshotRegistry.Clear(); + } + + public async ValueTask GetCurrentSolutionAsync(CancellationToken cancellationToken) + { + // First, calls to the client to get the current snapshot id. + // The client service calls the LSP client, which sends message to the LSP server, which in turn calls back to RegisterSolutionSnapshot. + // Once complete the snapshot should be registered. + var id = await InvokeAsync((service, cancellationToken) => service.RegisterSolutionSnapshotAsync(cancellationToken), cancellationToken).ConfigureAwait(false); + + return solutionSnapshotRegistry.GetRegisteredSolutionSnapshot(id); + } +} diff --git a/src/VisualStudio/DevKit/Impl/EditAndContinue/VoidActiveStatementTrackingController.cs b/src/VisualStudio/DevKit/Impl/EditAndContinue/VoidActiveStatementTrackingController.cs new file mode 100644 index 000000000000..fc610bc51a62 --- /dev/null +++ b/src/VisualStudio/DevKit/Impl/EditAndContinue/VoidActiveStatementTrackingController.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[Shared] +[Export(typeof(IActiveStatementTrackingController))] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VoidActiveStatementTrackingController() : IActiveStatementTrackingController +{ + private static readonly ActiveStatementSpanProvider s_emptyActiveStatementProvider = async (_, _, _) => []; + + public void StartTracking(Solution solution, IActiveStatementSpanFactory spanProvider) + { + } + + public ActiveStatementSpanProvider GetSpanProvider(Solution solution) + => s_emptyActiveStatementProvider; + + public void EndTracking() + { + } +} diff --git a/src/VisualStudio/DevKit/Impl/Microsoft.VisualStudio.LanguageServices.DevKit.csproj b/src/VisualStudio/DevKit/Impl/Microsoft.VisualStudio.LanguageServices.DevKit.csproj index 6a3b2eae473f..3da17b67684b 100644 --- a/src/VisualStudio/DevKit/Impl/Microsoft.VisualStudio.LanguageServices.DevKit.csproj +++ b/src/VisualStudio/DevKit/Impl/Microsoft.VisualStudio.LanguageServices.DevKit.csproj @@ -15,7 +15,11 @@ - + + + + + diff --git a/src/VisualStudio/ExternalAccess/FSharp/InlineHints/IFSharpInlineHintsService.cs b/src/VisualStudio/ExternalAccess/FSharp/InlineHints/IFSharpInlineHintsService.cs index 6e2ac06ad864..cedbbb96d309 100644 --- a/src/VisualStudio/ExternalAccess/FSharp/InlineHints/IFSharpInlineHintsService.cs +++ b/src/VisualStudio/ExternalAccess/FSharp/InlineHints/IFSharpInlineHintsService.cs @@ -16,3 +16,10 @@ internal interface IFSharpInlineHintsService /// Task> GetInlineHintsAsync(Document document, TextSpan textSpan, CancellationToken cancellationToken); } + +/// +internal interface IFSharpInlineHintsService2 +{ + /// + Task> GetInlineHintsAsync(Document document, TextSpan textSpan, bool displayAllOverride, CancellationToken cancellationToken); +} diff --git a/src/VisualStudio/ExternalAccess/FSharp/Internal/InlineHints/FSharpInlineHintsService.cs b/src/VisualStudio/ExternalAccess/FSharp/Internal/InlineHints/FSharpInlineHintsService.cs index adb7ca65e3eb..93aad4c2719c 100644 --- a/src/VisualStudio/ExternalAccess/FSharp/Internal/InlineHints/FSharpInlineHintsService.cs +++ b/src/VisualStudio/ExternalAccess/FSharp/Internal/InlineHints/FSharpInlineHintsService.cs @@ -17,23 +17,34 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.FSharp.Internal.InlineHints; [ExportLanguageService(typeof(IInlineHintsService), LanguageNames.FSharp), Shared] internal class FSharpInlineHintsService : IInlineHintsService { + private readonly IFSharpInlineHintsService2? _service2; private readonly IFSharpInlineHintsService? _service; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public FSharpInlineHintsService( + [Import(AllowDefault = true)] IFSharpInlineHintsService2? service2, [Import(AllowDefault = true)] IFSharpInlineHintsService? service) { + _service2 = service2; _service = service; } public async Task> GetInlineHintsAsync( Document document, TextSpan textSpan, InlineHintsOptions options, bool displayAllOverride, CancellationToken cancellationToken) { - if (_service == null) - return []; + if (_service2 != null) + { + var hints = await _service2.GetInlineHintsAsync(document, textSpan, displayAllOverride, cancellationToken).ConfigureAwait(false); + return hints.SelectAsArray(h => new InlineHint(h.Span, h.DisplayParts, (d, c) => h.GetDescriptionAsync(d, c))); + } - var hints = await _service.GetInlineHintsAsync(document, textSpan, cancellationToken).ConfigureAwait(false); - return hints.SelectAsArray(h => new InlineHint(h.Span, h.DisplayParts, (d, c) => h.GetDescriptionAsync(d, c))); + if (_service != null) + { + var hints = await _service.GetInlineHintsAsync(document, textSpan, cancellationToken).ConfigureAwait(false); + return hints.SelectAsArray(h => new InlineHint(h.Span, h.DisplayParts, (d, c) => h.GetDescriptionAsync(d, c))); + } + + return []; } } diff --git a/src/VisualStudio/ExternalAccess/FSharp/InternalAPI.Unshipped.txt b/src/VisualStudio/ExternalAccess/FSharp/InternalAPI.Unshipped.txt index 7b4664588f17..7e0790c7be3d 100644 --- a/src/VisualStudio/ExternalAccess/FSharp/InternalAPI.Unshipped.txt +++ b/src/VisualStudio/ExternalAccess/FSharp/InternalAPI.Unshipped.txt @@ -295,6 +295,8 @@ Microsoft.CodeAnalysis.ExternalAccess.FSharp.InlineHints.FSharpInlineHint.FSharp Microsoft.CodeAnalysis.ExternalAccess.FSharp.InlineHints.FSharpInlineHint.GetDescriptionAsync(Microsoft.CodeAnalysis.Document! document, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! Microsoft.CodeAnalysis.ExternalAccess.FSharp.InlineHints.IFSharpInlineHintsService Microsoft.CodeAnalysis.ExternalAccess.FSharp.InlineHints.IFSharpInlineHintsService.GetInlineHintsAsync(Microsoft.CodeAnalysis.Document! document, Microsoft.CodeAnalysis.Text.TextSpan textSpan, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! +Microsoft.CodeAnalysis.ExternalAccess.FSharp.InlineHints.IFSharpInlineHintsService2 +Microsoft.CodeAnalysis.ExternalAccess.FSharp.InlineHints.IFSharpInlineHintsService2.GetInlineHintsAsync(Microsoft.CodeAnalysis.Document! document, Microsoft.CodeAnalysis.Text.TextSpan textSpan, bool displayAllOverride, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! Microsoft.CodeAnalysis.ExternalAccess.FSharp.LanguageServices.FSharpVisualStudioWorkspaceExtensions Microsoft.CodeAnalysis.ExternalAccess.FSharp.NavigateTo.FSharpNavigateToItemKind Microsoft.CodeAnalysis.ExternalAccess.FSharp.NavigateTo.FSharpNavigateToMatchKind diff --git a/src/VisualStudio/ExternalAccess/FSharp/VS/IFSharpWorkspaceProjectContextFactory.cs b/src/VisualStudio/ExternalAccess/FSharp/VS/IFSharpWorkspaceProjectContextFactory.cs index 0bd4707a230d..2cfbebac7388 100644 --- a/src/VisualStudio/ExternalAccess/FSharp/VS/IFSharpWorkspaceProjectContextFactory.cs +++ b/src/VisualStudio/ExternalAccess/FSharp/VS/IFSharpWorkspaceProjectContextFactory.cs @@ -110,7 +110,9 @@ public FSharpWorkspaceProjectContext(IWorkspaceProjectContext vsProjectContext) } public void Dispose() +#pragma warning disable CS0618 // Type or member is obsolete => _vsProjectContext.Dispose(); +#pragma warning restore CS0618 // Type or member is obsolete public IVsLanguageServiceBuildErrorReporter2? BuildErrorReporter => _vsProjectContext as IVsLanguageServiceBuildErrorReporter2; diff --git a/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj b/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj index 07f62dfd1401..96979345b6ee 100644 --- a/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj +++ b/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj @@ -200,7 +200,7 @@ ServicesVisualStudio - BuiltProjectOutputGroup;PkgDefProjectOutputGroup;VsdConfigOutputGroup;SatelliteDllsProjectOutputGroup + BuiltProjectOutputGroup;PkgDefProjectOutputGroup;ContentFilesProjectOutputGroup;VsdConfigOutputGroup;SatelliteDllsProjectOutputGroup true BindingRedirect 2 diff --git a/src/VisualStudio/TestUtilities2/ProjectSystemShim/CSharpHelpers/CSharpHelpers.vb b/src/VisualStudio/TestUtilities2/ProjectSystemShim/CSharpHelpers/CSharpHelpers.vb new file mode 100644 index 000000000000..634bd033599e --- /dev/null +++ b/src/VisualStudio/TestUtilities2/ProjectSystemShim/CSharpHelpers/CSharpHelpers.vb @@ -0,0 +1,32 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports System.IO +Imports Microsoft.VisualStudio.LanguageServices.CSharp.ProjectSystemShim +Imports Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Framework +Imports Microsoft.VisualStudio.Shell.Interop + +Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.CSharpHelpers + + Friend Module CSharpHelpers + + Public Function CreateCSharpProject(environment As TestEnvironment, projectName As String) As CSharpProjectShim + Dim projectBinPath = Path.GetTempPath() + Dim hierarchy = environment.CreateHierarchy(projectName, projectBinPath, projectRefPath:=Nothing, projectCapabilities:="CSharp") + + Return CreateCSharpProject(environment, projectName, hierarchy) + End Function + + Public Function CreateCSharpProject(environment As TestEnvironment, projectName As String, hierarchy As IVsHierarchy) As CSharpProjectShim + Return New CSharpProjectShim( + New MockCSharpProjectRoot(hierarchy), + projectSystemName:=projectName, + hierarchy:=hierarchy, + serviceProvider:=environment.ServiceProvider, + threadingContext:=environment.ThreadingContext) + End Function + + End Module + +End Namespace diff --git a/src/VisualStudio/TestUtilities2/ProjectSystemShim/CSharpHelpers/MockCSharpProjectRoot.vb b/src/VisualStudio/TestUtilities2/ProjectSystemShim/CSharpHelpers/MockCSharpProjectRoot.vb new file mode 100644 index 000000000000..7f111b4ddb4c --- /dev/null +++ b/src/VisualStudio/TestUtilities2/ProjectSystemShim/CSharpHelpers/MockCSharpProjectRoot.vb @@ -0,0 +1,75 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports Microsoft.VisualStudio.LanguageServices.CSharp.ProjectSystemShim.Interop +Imports Microsoft.VisualStudio.Shell.Interop + +Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.CSharpHelpers + + Friend NotInheritable Class MockCSharpProjectRoot + Implements ICSharpProjectRoot + + Private ReadOnly _hierarchy As IVsHierarchy + + Public Sub New(hierarchy As IVsHierarchy) + _hierarchy = hierarchy + End Sub + + Private Function BelongsToProject(pszFileName As String) As Integer Implements ICSharpProjectRoot.BelongsToProject + Throw New NotImplementedException() + End Function + + Private Function BuildPerConfigCacheFileName() As String Implements ICSharpProjectRoot.BuildPerConfigCacheFileName + Throw New NotImplementedException() + End Function + + Private Function CanCreateFileCodeModel(pszFile As String) As Boolean Implements ICSharpProjectRoot.CanCreateFileCodeModel + Throw New NotImplementedException() + End Function + + Private Sub ConfigureCompiler(compiler As ICSCompiler, inputSet As ICSInputSet, addSources As Boolean) Implements ICSharpProjectRoot.ConfigureCompiler + Throw New NotImplementedException() + End Sub + + Private Function CreateFileCodeModel(pszFile As String, ByRef riid As Guid) As Object Implements ICSharpProjectRoot.CreateFileCodeModel + Throw New NotImplementedException() + End Function + + Private Function GetActiveConfigurationName() As String Implements ICSharpProjectRoot.GetActiveConfigurationName + Throw New NotImplementedException() + End Function + + Private Function GetFullProjectName() As String Implements ICSharpProjectRoot.GetFullProjectName + Throw New NotImplementedException() + End Function + + Private Function GetHierarchyAndItemID(pszFile As String, ByRef ppHier As IVsHierarchy, ByRef pItemID As UInteger) As Integer Implements ICSharpProjectRoot.GetHierarchyAndItemID + ppHier = _hierarchy + + ' Each item should have it's own ItemID, but for simplicity we'll just hard-code a value of + ' no particular significance. + pItemID = 42 + + Return VSConstants.S_OK + End Function + + Private Sub GetHierarchyAndItemIDOptionallyInProject(pszFile As String, ByRef ppHier As IVsHierarchy, ByRef pItemID As UInteger, mustBeInProject As Boolean) Implements ICSharpProjectRoot.GetHierarchyAndItemIDOptionallyInProject + Throw New NotImplementedException() + End Sub + + Private Function GetProjectLocation() As String Implements ICSharpProjectRoot.GetProjectLocation + Throw New NotImplementedException() + End Function + + Private Function GetProjectSite(ByRef riid As Guid) As Object Implements ICSharpProjectRoot.GetProjectSite + Throw New NotImplementedException() + End Function + + Private Sub SetProjectSite(site As ICSharpProjectSite) Implements ICSharpProjectRoot.SetProjectSite + Throw New NotImplementedException() + End Sub + + End Class + +End Namespace diff --git a/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb b/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb index 5967e4786e99..e3baf648d23d 100644 --- a/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb +++ b/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb @@ -5,7 +5,6 @@ Imports System.ComponentModel.Composition Imports System.ComponentModel.Composition.Hosting Imports System.IO -Imports System.Runtime.InteropServices Imports System.Threading Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Diagnostics @@ -23,6 +22,7 @@ Imports Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem Imports Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.CPS Imports Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Legacy Imports Microsoft.VisualStudio.LanguageServices.Implementation.TaskList +Imports Microsoft.VisualStudio.LanguageServices.TaskList Imports Microsoft.VisualStudio.LanguageServices.Telemetry Imports Microsoft.VisualStudio.LanguageServices.UnitTests.CodeModel Imports Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics @@ -72,7 +72,8 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Fr GetType(OpenTextBufferProvider), GetType(StubVsEditorAdaptersFactoryService), GetType(ExternalErrorDiagnosticUpdateSource), - GetType(MockServiceBroker)) + GetType(MockServiceBroker), + GetType(VisualStudioDiagnosticIdCacheFactory)) Private ReadOnly _workspace As VisualStudioWorkspaceImpl Private ReadOnly _projectFilePaths As New List(Of String) diff --git a/src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb b/src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb index 3ebc391763ae..625bab401516 100644 --- a/src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb +++ b/src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb @@ -90,13 +90,11 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic End Sub Protected Overrides Sub RegisterObjectBrowserLibraryManager() - Dim workspace As VisualStudioWorkspace = ComponentModel.GetService(Of VisualStudioWorkspace)() - Contract.ThrowIfFalse(JoinableTaskFactory.Context.IsOnMainThread) Dim objectManager = TryCast(GetService(GetType(SVsObjectManager)), IVsObjectManager2) If objectManager IsNot Nothing Then - Me._libraryManager = New ObjectBrowserLibraryManager(Me, ComponentModel, workspace) + Me._libraryManager = New ObjectBrowserLibraryManager(Me, ComponentModel) If ErrorHandler.Failed(objectManager.RegisterSimpleLibrary(Me._libraryManager, Me._libraryManagerCookie)) Then Me._libraryManagerCookie = 0 diff --git a/src/VisualStudio/VisualBasic/Impl/ObjectBrowser/ObjectBrowserLibraryManager.vb b/src/VisualStudio/VisualBasic/Impl/ObjectBrowser/ObjectBrowserLibraryManager.vb index 413785014156..a89bf8261fe4 100644 --- a/src/VisualStudio/VisualBasic/Impl/ObjectBrowser/ObjectBrowserLibraryManager.vb +++ b/src/VisualStudio/VisualBasic/Impl/ObjectBrowser/ObjectBrowserLibraryManager.vb @@ -11,8 +11,8 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.ObjectBrowser Friend NotInheritable Class ObjectBrowserLibraryManager Inherits AbstractObjectBrowserLibraryManager - Public Sub New(serviceProvider As IServiceProvider, componentModel As IComponentModel, workspace As VisualStudioWorkspace) - MyBase.New(LanguageNames.VisualBasic, Guids.VisualBasicLibraryId, serviceProvider, componentModel, workspace) + Public Sub New(serviceProvider As IServiceProvider, componentModel As IComponentModel) + MyBase.New(LanguageNames.VisualBasic, Guids.VisualBasicLibraryId, serviceProvider, componentModel) End Sub Friend Overrides Function CreateDescriptionBuilder( diff --git a/src/Workspaces/CSharp/Portable/Classification/ClassificationHelpers.cs b/src/Workspaces/CSharp/Portable/Classification/ClassificationHelpers.cs index 125fe77edd70..479f4ca472b8 100644 --- a/src/Workspaces/CSharp/Portable/Classification/ClassificationHelpers.cs +++ b/src/Workspaces/CSharp/Portable/Classification/ClassificationHelpers.cs @@ -296,6 +296,9 @@ private static bool IsVerbatimStringToken(SyntaxToken token) SyntaxKind.RecordDeclaration => ClassificationTypeNames.RecordClassName, SyntaxKind.RecordStructDeclaration => ClassificationTypeNames.RecordStructName, SyntaxKind.StructDeclaration => ClassificationTypeNames.StructName, + // Tracked by https://github.com/dotnet/roslyn/issues/82607 + // Consider using a separate classification type for unions so users can color them differently + SyntaxKind.UnionDeclaration => ClassificationTypeNames.StructName, _ => null }; @@ -342,6 +345,7 @@ public static bool IsStaticallyDeclared(SyntaxToken token) SyntaxKind.ClassDeclaration => ClassificationTypeNames.ClassName, SyntaxKind.EnumDeclaration => ClassificationTypeNames.EnumName, SyntaxKind.StructDeclaration => ClassificationTypeNames.StructName, + SyntaxKind.UnionDeclaration => ClassificationTypeNames.StructName, SyntaxKind.InterfaceDeclaration => ClassificationTypeNames.InterfaceName, SyntaxKind.RecordDeclaration => ClassificationTypeNames.RecordClassName, SyntaxKind.RecordStructDeclaration => ClassificationTypeNames.RecordStructName, diff --git a/src/Workspaces/CSharp/Portable/Classification/SyntaxClassification/DocCommentCodeBlockClassifier.cs b/src/Workspaces/CSharp/Portable/Classification/SyntaxClassification/DocCommentCodeBlockClassifier.cs index 422cfbc6365c..8a9582fb87b8 100644 --- a/src/Workspaces/CSharp/Portable/Classification/SyntaxClassification/DocCommentCodeBlockClassifier.cs +++ b/src/Workspaces/CSharp/Portable/Classification/SyntaxClassification/DocCommentCodeBlockClassifier.cs @@ -145,7 +145,7 @@ private static void AddTestCodeBackgroundClassification(SegmentedList 0 and var firstSpaceIndex) + var contentText = node.Content.Text.AsSpan(); + var firstWhitespaceIndex = contentText.IndexOfAny([' ', '\t']); + + if (firstWhitespaceIndex <= 0) { - var keywordSpan = new TextSpan(node.Content.SpanStart, firstSpaceIndex); - var stringLiteralSpan = TextSpan.FromBounds(node.Content.SpanStart + firstSpaceIndex, node.Content.FullSpan.End); + // Only have a 'kind' here. + // #:kind + // ^^^^ + AddClassification(node.Content, ClassificationTypeNames.PreprocessorKeyword); + ClassifyDirectiveTrivia(node); + return; + } - AddClassification(keywordSpan, ClassificationTypeNames.PreprocessorKeyword); - AddClassification(stringLiteralSpan, ClassificationTypeNames.StringLiteral); + // #:kind name=value + // ^^^^ + AddClassification(new TextSpan(node.Content.SpanStart, firstWhitespaceIndex), ClassificationTypeNames.PreprocessorKeyword); + + // Skip whitespace between 'kind' and 'name' + var nameStart = firstWhitespaceIndex; + while (nameStart < contentText.Length && (contentText[nameStart] == ' ' || contentText[nameStart] == '\t')) + { + nameStart++; } - else + + if (nameStart < contentText.Length) { - AddClassification(node.Content, ClassificationTypeNames.PreprocessorKeyword); + var directiveKind = contentText[..firstWhitespaceIndex]; + if (directiveKind.Equals("sdk".AsSpan(), StringComparison.Ordinal) + || directiveKind.Equals("package".AsSpan(), StringComparison.Ordinal)) + { + // #:kind name@value + // ^^^^^^^^^^ + ClassifyAppDirectiveNameAndOptionalSeparatorValue(node.Content.SpanStart, contentText, nameStart, '@'); + } + else if (directiveKind.Equals("property".AsSpan(), StringComparison.Ordinal)) + { + // #:kind name=value + // ^^^^^^^^^^ + ClassifyAppDirectiveNameAndOptionalSeparatorValue(node.Content.SpanStart, contentText, nameStart, '='); + } + else + { + // #:kind name + // ^^^^ + AddClassification(new TextSpan(node.Content.SpanStart + nameStart, contentText.Length - nameStart), ClassificationTypeNames.StringLiteral); + } } ClassifyDirectiveTrivia(node); } + private void ClassifyAppDirectiveNameAndOptionalSeparatorValue(int contentStart, ReadOnlySpan contentText, int nameStart, char separator) + { + var separatorIndex = contentText[nameStart..].IndexOf(separator); + if (separatorIndex == -1) + { + // Only have a name + ClassifyDottedName(contentStart, contentText, nameStart, contentText.Length); + return; + } + + // Adjust 'separatorIndex' to be relative to 'contentText' + separatorIndex += nameStart; + + // my.name=value + // ^^^^^^^ + ClassifyDottedName(contentStart, contentText, nameStart, separatorIndex); + + // my.name=value + // ^ + AddClassification(new TextSpan(contentStart + separatorIndex, 1), ClassificationTypeNames.Punctuation); + + var valueIndex = separatorIndex + 1; + if (valueIndex < contentText.Length) + { + // my.name=value + // ^^^^^ + AddClassification(new TextSpan(contentStart + valueIndex, contentText.Length - valueIndex), ClassificationTypeNames.StringLiteral); + } + } + + private void ClassifyDottedName(int contentStart, ReadOnlySpan contentText, int start, int end) + { + var segmentStart = start; + for (var index = start; index < end; index++) + { + if (contentText[index] != '.') + { + continue; + } + + if (index > segmentStart) + { + // left.right + // ^^^^ + AddClassification(new TextSpan(contentStart + segmentStart, index - segmentStart), ClassificationTypeNames.Identifier); + } + + // left.right + // ^ + AddClassification(new TextSpan(contentStart + index, 1), ClassificationTypeNames.Punctuation); + segmentStart = index + 1; + } + + if (end > segmentStart) + { + // left.right + // ^^^^^ + AddClassification(new TextSpan(contentStart + segmentStart, end - segmentStart), ClassificationTypeNames.Identifier); + } + } + private void ClassifyNullableDirective(NullableDirectiveTriviaSyntax node) { AddClassification(node.HashToken, ClassificationTypeNames.PreprocessorKeyword); diff --git a/src/Workspaces/CSharp/Portable/FindSymbols/FindSymbolsUtilities.cs b/src/Workspaces/CSharp/Portable/FindSymbols/FindSymbolsUtilities.cs index 35306414207a..155ce1f9f13b 100644 --- a/src/Workspaces/CSharp/Portable/FindSymbols/FindSymbolsUtilities.cs +++ b/src/Workspaces/CSharp/Portable/FindSymbols/FindSymbolsUtilities.cs @@ -63,6 +63,7 @@ public static DeclaredSymbolInfoKind GetDeclaredSymbolInfoKind(TypeDeclarationSy SyntaxKind.StructDeclaration => DeclaredSymbolInfoKind.Struct, SyntaxKind.RecordDeclaration => DeclaredSymbolInfoKind.Record, SyntaxKind.RecordStructDeclaration => DeclaredSymbolInfoKind.RecordStruct, + SyntaxKind.UnionDeclaration => DeclaredSymbolInfoKind.Union, _ => throw ExceptionUtilities.UnexpectedValue(typeDeclaration.Kind()), }; } diff --git a/src/Workspaces/CSharpTest/Formatting/FormattingTests.cs b/src/Workspaces/CSharpTest/Formatting/FormattingTests.cs index 20280e0c3674..0634feab767a 100644 --- a/src/Workspaces/CSharpTest/Formatting/FormattingTests.cs +++ b/src/Workspaces/CSharpTest/Formatting/FormattingTests.cs @@ -11072,6 +11072,17 @@ struct R(int X) ; """); + [Fact] + public Task Union_01() + => AssertFormatAsync( + """ + union U(T, string) { } + """, + """ + union U < T > ( T ,string ) { } + """, + parseOptions: CSharpParseOptions.Default.WithLanguageVersion(LanguageVersionExtensions.CSharpNext)); + [Fact] public async Task FormatListPattern() { @@ -12752,4 +12763,122 @@ public Task FormatKeyValuePair3() var v = [ null : 1 + 1 , ( x . y ) : from x in y select z ]; """); #endif + + [Fact] + public Task StandaloneBlocksShouldNotCollapseWhenMethodBraceOptionIsOff() + => AssertFormatAsync( + expected: """ + class C + { + void M() { + { + } + { + } + } + } + """, + code: """ + class C + { + void M() + { + { + } + { + } + } + } + """, + changedOptionSet: new OptionsCollection(LanguageNames.CSharp) + { + { NewLineBeforeOpenBrace, NewLineBeforeOpenBrace.DefaultValue.WithFlagValue(NewLineBeforeOpenBracePlacement.Methods, false) } + }); + + [Fact] + public Task StandaloneBlocksShouldNotCollapseWhenAllBraceOptionsOff() + => AssertFormatAsync( + expected: """ + class C { + void M() { + { + } + { + } + } + } + """, + code: """ + class C + { + void M() + { + { + } + { + } + } + } + """, + changedOptionSet: new OptionsCollection(LanguageNames.CSharp) + { + { NewLineBeforeOpenBrace, NewLineBeforeOpenBracePlacement.None } + }); + + [Fact] + public Task TopLevelStandaloneBlocksShouldNotCollapseWhenMethodBraceOptionIsOff() + => AssertFormatAsync( + expected: """ + { + } + { + } + """, + code: """ + { + } + { + } + """, + changedOptionSet: new OptionsCollection(LanguageNames.CSharp) + { + { NewLineBeforeOpenBrace, NewLineBeforeOpenBrace.DefaultValue.WithFlagValue(NewLineBeforeOpenBracePlacement.Methods, false) } + }); + + [Fact] + public Task TopLevelStandaloneBlocksShouldNotCollapseWhenAllBraceOptionsOff() + => AssertFormatAsync( + expected: """ + { + } + { + } + """, + code: """ + { + } + { + } + """, + changedOptionSet: new OptionsCollection(LanguageNames.CSharp) + { + { NewLineBeforeOpenBrace, NewLineBeforeOpenBracePlacement.None } + }); + + [Fact] + public Task TopLevelLocalFunctionBraceStillFormatsWhenMethodBraceOptionIsOff() + => AssertFormatAsync( + expected: """ + void M() { + } + """, + code: """ + void M() + { + } + """, + changedOptionSet: new OptionsCollection(LanguageNames.CSharp) + { + { NewLineBeforeOpenBrace, NewLineBeforeOpenBrace.DefaultValue.WithFlagValue(NewLineBeforeOpenBracePlacement.Methods, false) } + }); } diff --git a/src/Workspaces/CSharpTest/Formatting/FormattingTriviaTests.cs b/src/Workspaces/CSharpTest/Formatting/FormattingTriviaTests.cs index 02fd2e4a1634..285ae3073aa7 100644 --- a/src/Workspaces/CSharpTest/Formatting/FormattingTriviaTests.cs +++ b/src/Workspaces/CSharpTest/Formatting/FormattingTriviaTests.cs @@ -5,6 +5,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -53,6 +54,32 @@ public Task PreprocessorInEmptyFile() #error """); + [Fact] + public Task FileBasedAppDirective() + { + var parseOptions = CSharpParseOptions.Default.WithFeatures([new KeyValuePair("FileBasedProgram", "true")]); + return AssertNoFormattingChangesAsync(""" + #:package Newtonsoft.Json@13.0.3 + + Console.WriteLine("Hello"); + """, parseOptions: parseOptions); + } + + [Fact] + public Task FileBasedAppDirective_WithLeadingWhitespace() + { + var parseOptions = CSharpParseOptions.Default.WithFeatures([new KeyValuePair("FileBasedProgram", "true")]); + return AssertFormatAsync(""" + #:package Newtonsoft.Json@13.0.3 + + Console.WriteLine("Hello"); + """, """ + #:package Newtonsoft.Json@13.0.3 + + Console.WriteLine("Hello"); + """, parseOptions: parseOptions); + } + [Fact] public Task Comment1() => AssertFormatAsync(""" diff --git a/src/Workspaces/CSharpTest/Microsoft.CodeAnalysis.CSharp.Workspaces.UnitTests.csproj b/src/Workspaces/CSharpTest/Microsoft.CodeAnalysis.CSharp.Workspaces.UnitTests.csproj index 065c2a4dd7ae..8a012c770a64 100644 --- a/src/Workspaces/CSharpTest/Microsoft.CodeAnalysis.CSharp.Workspaces.UnitTests.csproj +++ b/src/Workspaces/CSharpTest/Microsoft.CodeAnalysis.CSharp.Workspaces.UnitTests.csproj @@ -5,7 +5,7 @@ Library Microsoft.CodeAnalysis.CSharp.UnitTests - $(NetVSShared);net472 + $(NetRoslynWindowsTests);net472 diff --git a/src/Workspaces/Core/Portable/Diagnostics/HostDiagnosticAnalyzers.cs b/src/Workspaces/Core/Portable/Diagnostics/HostDiagnosticAnalyzers.cs index f72d4e1095e7..47f155ba8072 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/HostDiagnosticAnalyzers.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/HostDiagnosticAnalyzers.cs @@ -75,6 +75,40 @@ internal HostDiagnosticAnalyzers(IReadOnlyList hostAnalyzerRe public ImmutableDictionary> GetOrCreateHostDiagnosticAnalyzersPerReference(string language) => _hostDiagnosticAnalyzersPerLanguageMap.GetOrAdd(language, CreateHostDiagnosticAnalyzersAndBuildMap); + /// + /// Returns all the DiagnosticIds producible by the referenced DiagnosticAnalyzers. + /// + public ImmutableDictionary> GetAllDiagnosticIds( + DiagnosticAnalyzerInfoCache infoCache, + ImmutableArray projects) + { + var builder = ImmutableDictionary.CreateBuilder>(); + + foreach (var project in projects) + { + var diagnosticIds = GetAllDiagnosticIds(infoCache, project); + builder.Add(project.Id, diagnosticIds); + } + + return builder.ToImmutable(); + + ImmutableHashSet GetAllDiagnosticIds(DiagnosticAnalyzerInfoCache infoCache, Project project) + { + var descriptorsPerReference = GetDiagnosticDescriptorsPerReference(infoCache, project); + + var diagnosticIdBuilder = ImmutableHashSet.CreateBuilder(); + foreach (var descriptors in descriptorsPerReference.Values) + { + foreach (var descriptor in descriptors) + { + diagnosticIdBuilder.Add(descriptor.Id); + } + } + + return diagnosticIdBuilder.ToImmutable(); + } + } + public ImmutableDictionary> GetDiagnosticDescriptorsPerReference( DiagnosticAnalyzerInfoCache infoCache, Project? project) diff --git a/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs b/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs index 2675e961f7ba..d81e219c74a1 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/IRemoteDiagnosticAnalyzerService.cs @@ -51,6 +51,9 @@ ValueTask> GetDiagnosticDescriptorsAsyn ValueTask> GetCompilationEndDiagnosticDescriptorIdsAsync( Checksum solutionChecksum, CancellationToken cancellationToken); + ValueTask>> GetAllDiagnosticIdsAsync( + Checksum solutionChecksum, ImmutableArray projectIds, CancellationToken cancellationToken); + ValueTask>> GetDiagnosticDescriptorsPerReferenceAsync( Checksum solutionChecksum, ProjectId? projectId, CancellationToken cancellationToken); } diff --git a/src/Workspaces/Core/Portable/FindSymbols/Declarations/DeclarationFinder_SourceDeclarations.cs b/src/Workspaces/Core/Portable/FindSymbols/Declarations/DeclarationFinder_SourceDeclarations.cs index 27a662f79842..14956b46cc1f 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/Declarations/DeclarationFinder_SourceDeclarations.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/Declarations/DeclarationFinder_SourceDeclarations.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -228,7 +228,7 @@ private static async Task> FindSourceDeclarationsWithPat // Ok, we had a dotted pattern. Have to see if the symbol's container matches the // pattern as well. - using var containerPatternMatcher = PatternMatcher.CreateDotSeparatedContainerMatcher(containerPart); + using var containerPatternMatcher = PatternMatcher.CreateDotSeparatedContainerMatcher(containerPart, includeMatchedSpans: false); return symbolAndProjectIds.WhereAsArray(t => containerPatternMatcher.Matches(GetContainer(t))); diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder_ProjectIndex.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder_ProjectIndex.cs index 8d6fec248ec0..dbda8b140874 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder_ProjectIndex.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder_ProjectIndex.cs @@ -102,6 +102,7 @@ private static async Task CreateIndexAsync(Project project, Cancel break; case DeclaredSymbolInfoKind.Struct: case DeclaredSymbolInfoKind.RecordStruct: + case DeclaredSymbolInfoKind.Union: valueTypes.Add(documentId, info); break; case DeclaredSymbolInfoKind.Delegate: diff --git a/src/Workspaces/Core/Portable/FindSymbols/Shared/AbstractSyntaxIndex_Persistence.cs b/src/Workspaces/Core/Portable/FindSymbols/Shared/AbstractSyntaxIndex_Persistence.cs index b9394430dbed..9d839574688d 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/Shared/AbstractSyntaxIndex_Persistence.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/Shared/AbstractSyntaxIndex_Persistence.cs @@ -23,7 +23,7 @@ internal abstract partial class AbstractSyntaxIndex /// that we will not try to read previously cached data from a prior version of roslyn with a different format and /// will instead regenerate all the indices with the new format. /// - private static readonly Checksum s_serializationFormatChecksum = CodeAnalysis.Checksum.Create("51"); + private static readonly Checksum s_serializationFormatChecksum = CodeAnalysis.Checksum.Create("54"); /// /// Cache of ParseOptions to a checksum for the contained diff --git a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/DeclaredSymbolInfo.cs b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/DeclaredSymbolInfo.cs index a35454805107..6fd7b0ac95ef 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/DeclaredSymbolInfo.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/DeclaredSymbolInfo.cs @@ -15,6 +15,9 @@ namespace Microsoft.CodeAnalysis.FindSymbols; +/// +/// When adding a new value, also bump . +/// internal enum DeclaredSymbolInfoKind : byte { Class, @@ -36,6 +39,7 @@ internal enum DeclaredSymbolInfoKind : byte Record, RecordStruct, Struct, + Union, } [DataContract] diff --git a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex.NavigateToSearchInfo.cs b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex.NavigateToSearchInfo.cs new file mode 100644 index 000000000000..f1c574edd208 --- /dev/null +++ b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex.NavigateToSearchInfo.cs @@ -0,0 +1,935 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.PatternMatching; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.FindSymbols; + +internal sealed partial class NavigateToSearchIndex +{ + /// + /// Pre-computed data used by NavigateTo to quickly pre-filter documents before performing + /// expensive per-symbol pattern matching. Contains: + /// + /// A for symbol name hump first-characters and hump-initial bigrams. + /// A bloom filter for lowercased prefixes of each hump (for all-lowercase CamelCase matching). + /// A bloom filter for lowercased trigrams of symbol name word-parts. + /// A for container name hump first-characters. + /// A length bitset indicating which symbol name lengths exist in the document. + /// + /// + private readonly struct NavigateToSearchInfo + { + /// + /// The maximum bit index in . Since the bitset is a 64-bit + /// , indices 0..62 map 1-to-1 to symbol lengths, and all lengths ≥ 63 are + /// bucketed into bit 63. + /// + private const int MaxSymbolNameLengthBitIndex = 63; + + /// + /// The false positive probability for bloom filters, matching the value used by for find-references bloom filters. + /// + private const double FalsePositiveProbability = 0.0001; + + /// + /// Exact set storing data derived from symbol name hump structure: + /// + /// Uppercased first characters of each character-part (e.g. for "GooBar" stores "G" and "B"; + /// for "XMLDocument" stores "X", "M", "L", "D"). Used for single-hump pattern checks (e.g. + /// pattern "Goo" checks that "G" is stored) and as the base case for all-lowercase patterns. + /// All ordered pairs of uppercased hump-initial characters. For example, "GooBarQuux" has + /// hump initials G, B, Q, so we store all ordered pairs: "GB", "GQ", "BQ". At query time, a + /// multi-hump pattern like "GBQ" is validated by checking that each adjacent pair of the + /// pattern's hump initials ("GB" and "BQ") is present. Because the index stores all + /// ordered pairs (not just adjacent), this also handles non-contiguous CamelCase matches: pattern + /// "GQ" checks the single pair "GQ", which is stored even though G and Q are not adjacent humps + /// in the candidate. + /// + /// For English identifiers the domain is at most 702 values (26 single chars + 26×26 bigrams); + /// non-English Unicode identifiers may add more, but the set only contains initials actually + /// present in the document, so it stays small in practice. A provides + /// exact membership with zero false positives, negligible memory, and fast lookups optimized for + /// read-heavy access. + /// + private readonly FrozenSet _humpSet; + + /// + /// Bloom filter storing lowercased prefixes of each character-part (hump). For example, + /// "GooBar" has character-parts "Goo" and "Bar", so we store "g", "go", "goo", "b", "ba", + /// "bar". At query time for an all-lowercase pattern like "goba", a DP tries every way to + /// split the pattern into segments and checks whether each segment is a stored hump prefix. + /// The split "go"+"ba" succeeds because "go" is a prefix of "Goo" and "ba" is a prefix of + /// "Bar". This is much more selective than checking a single character: instead of a small + /// set of possible initial values (26 for English, slightly more with Unicode), each segment is + /// checked against a multi-character string space. A key optimization: + /// if segment pattern[i..j) is not in the filter, we can skip all longer extensions + /// pattern[i..j+1), pattern[i..j+2), etc., because no hump can have a longer prefix without + /// also having the shorter one (and bloom filters have no false negatives). + /// + private readonly BloomFilter _humpPrefixFilter; + + /// + /// Bloom filter storing lowercased trigrams (3-character sliding windows) of each word-part. + /// For example, for "Readline" stores "rea", "ead", "adl", "dli", "lin", "ine". Used to detect + /// matches like "line" + /// matching "Readline". + /// + private readonly BloomFilter _trigramFilter; + + /// + /// Exact set storing uppercased first characters of each character-part across all segments of + /// the fully-qualified container name. For example, "System.GooBar.Quux" produces hump initials + /// 'S', 'G', 'B', 'Q'. A of chars handles any Unicode character (not + /// just A–Z) and provides exact membership with zero false positives and fast lookups. + /// + private readonly FrozenSet _containerCharSet; + + /// + /// A 64-bit bitset indicating which symbol name lengths exist in the document. Bit i is set + /// if some symbol has a name of length i (lengths ≥ 63 share bit ). + /// Used for fuzzy-match pre-filtering: a fuzzy match requires the candidate and pattern lengths to + /// differ by at most , so if no symbol length + /// is within that range, the document can be skipped. + /// + private readonly ulong _symbolNameLengthBitset; + + /// + /// Number of distinct character indices in the fuzzy bigram alphabet: lowercase letters a-z (26), + /// digits 0-9 (10), underscore (1), and a single "other" bucket for all remaining characters + /// (Unicode letters, etc.). + /// + private const int FuzzyBigramAlphabetSize = 26 + 10 + 1 + 1; + + /// + /// Total number of bits in the fuzzy bigram bitset: one bit per ordered character pair, giving + /// × = 1369 bits. + /// + private const int FuzzyBigramBitCount = FuzzyBigramAlphabetSize * FuzzyBigramAlphabetSize; + + /// + /// Number of elements needed to store bits. + /// + private const int FuzzyBigramUlongCount = (FuzzyBigramBitCount + 63) / 64; + + /// + /// Exact bitset storing lowercased bigrams (2-character sliding windows) of all symbol names in the + /// document. For example, "GooBar" contributes bigrams "go", "oo", "ob", "ba", "ar" (lowercased). + /// + /// Used for fuzzy-match pre-filtering via the q-gram count lemma (Ukkonen, 1992): each edit + /// operation can destroy at most q=2 bigrams, so if edit_distance(pattern, candidate) <= k, + /// then at least |pattern| - 1 - 2k of the pattern's bigrams must also be bigrams of the + /// candidate. If the count of matching bigrams falls below this threshold, the document can be + /// skipped for fuzzy matching. + /// + /// See: Ukkonen, E. (1992). "Approximate string-matching with q-grams and maximal matches." + /// Theoretical Computer Science, 92(1), 191–211. + /// + /// + /// Characters are mapped to a 38-element alphabet via : + /// a-z → 0..25, 0-9 → 26..35, '_' → 36, everything else → 37 ("other"). This gives a + /// 38×38 = 1444-bit bitset (23 ulongs, 184 bytes) — compact enough to store per-document with + /// near-exact precision. The "other" bucket means two distinct Unicode characters (e.g. 'α' and + /// 'β') hash to the same index, but this is rare in practice and only causes a slightly higher + /// false-positive rate for those characters. + /// + private readonly ImmutableArray _fuzzyBigramBitset; + + private NavigateToSearchInfo( + FrozenSet humpSet, + BloomFilter humpPrefixFilter, + BloomFilter trigramFilter, + FrozenSet containerCharSet, + ulong symbolNameLengthBitset, + ImmutableArray fuzzyBigramBitset) + { + _humpSet = humpSet; + _humpPrefixFilter = humpPrefixFilter; + _trigramFilter = trigramFilter; + _containerCharSet = containerCharSet; + _symbolNameLengthBitset = symbolNameLengthBitset; + _fuzzyBigramBitset = fuzzyBigramBitset; + } + + public static NavigateToSearchInfo Create(IReadOnlyList infos) + { + using var _1 = PooledHashSet.GetInstance(out var humpStrings); + using var _2 = PooledHashSet.GetInstance(out var humpPrefixStrings); + using var _3 = PooledHashSet.GetInstance(out var trigramStrings); + using var _4 = PooledHashSet.GetInstance(out var containerChars); + var lengthBitset = 0UL; + var fuzzyBigramBitset = new ulong[FuzzyBigramUlongCount]; + + // Find the longest name so we can allocate a single buffer for lowercasing. + var maxNameLength = 0; + foreach (var info in infos) + maxNameLength = Math.Max(maxNameLength, info.Name.Length); + + var rentedCharArray = maxNameLength > 256 + ? ArrayPool.Shared.Rent(maxNameLength) + : null; + + var buffer = rentedCharArray ?? stackalloc char[maxNameLength]; + + foreach (var info in infos) + { + AddNameData(info.Name, buffer[..info.Name.Length]); + AddContainerData(info.FullyQualifiedContainerName, containerChars); + } + + if (rentedCharArray is not null) + ArrayPool.Shared.Return(rentedCharArray); + + return new NavigateToSearchInfo( + humpStrings.ToFrozenSet(), + new BloomFilter(FalsePositiveProbability, isCaseSensitive: true, humpPrefixStrings), + new BloomFilter(FalsePositiveProbability, isCaseSensitive: true, trigramStrings), + containerChars.ToFrozenSet(), + lengthBitset, + ImmutableCollectionsMarshal.AsImmutableArray(fuzzyBigramBitset)); + + void AddNameData(string name, Span loweredName) + { + if (string.IsNullOrEmpty(name)) + return; + + // Record symbol name length in the bitset for fuzzy-match pre-filtering. + lengthBitset |= 1UL << Math.Min(name.Length, MaxSymbolNameLengthBitIndex); + + name.ToLowerInvariant(loweredName); + + using var charParts = TemporaryArray.Empty; + StringBreaker.AddCharacterParts(name, ref charParts.AsRef()); + + AddHumpData(); + AddHumpPrefixData(loweredName); + AddTrigramData(loweredName); + AddFuzzyBigramData(); + + void AddHumpData() + { + // Store individual hump-initial characters (uppercased). + foreach (var part in charParts) + AddToSet(humpStrings, [char.ToUpperInvariant(name[part.Start])]); + + // Store all ordered pairs of hump-initial characters (uppercased). + // For "GooBarQuux" (humps G, B, Q): stores "GB", "GQ", "BQ". + // Storing ALL pairs (not just adjacent) enables non-contiguous CamelCase matching + // like "GQ" matching "GooBarQuux" by skipping "Bar". + for (var i = 0; i < charParts.Count; i++) + { + var ci = char.ToUpperInvariant(name[charParts[i].Start]); + for (var j = i + 1; j < charParts.Count; j++) + { + var cj = char.ToUpperInvariant(name[charParts[j].Start]); + AddToSet(humpStrings, [ci, cj]); + } + } + } + + // Store lowercased prefixes of each character-part (hump). + // For "GooBar" → parts "Goo", "Bar" → stores "g", "go", "goo", "b", "ba", "bar". + // Used by the all-lowercase DP to check whether a pattern can be split into segments + // that each match a hump prefix. + void AddHumpPrefixData(ReadOnlySpan loweredName) + { + foreach (var part in charParts) + { + for (var prefixLen = 1; prefixLen <= part.Length; prefixLen++) + AddToSet(humpPrefixStrings, loweredName.Slice(part.Start, prefixLen)); + } + } + + void AddTrigramData(ReadOnlySpan loweredName) + { + // Break the name into word-parts and store lowercased trigrams (3-character sliding windows). + using var wordParts = TemporaryArray.Empty; + StringBreaker.AddWordParts(name, ref wordParts.AsRef()); + + foreach (var part in wordParts) + { + if (part.Length < 3) + continue; + + for (var i = 0; i + 3 <= part.Length; i++) + AddToSet(trigramStrings, loweredName.Slice(part.Start + i, 3)); + } + } + + // Populate the fuzzy bigram bitset with lowercased bigrams (2-character sliding windows) + // of the full name. For "GooBar" this stores: "go", "oo", "ob", "ba", "ar". + void AddFuzzyBigramData() + { + for (var i = 0; i < name.Length - 1; i++) + { + var idx = FuzzyBigramCharIndex(char.ToLowerInvariant(name[i])) * FuzzyBigramAlphabetSize + + FuzzyBigramCharIndex(char.ToLowerInvariant(name[i + 1])); + fuzzyBigramBitset[idx >> 6] |= 1UL << (idx & 63); + } + } + + static void AddToSet(HashSet set, ReadOnlySpan value) + { +#if NET9_0_OR_GREATER + set.GetAlternateLookup>().Add(value); +#else + set.Add(value.ToString()); +#endif + } + } + } + + /// + /// Stores individual hump-initial characters (uppercased) and all ordered pairs of + /// hump-initial characters. For "GooBarQuux" (humps G, B, Q): stores "G", "B", "Q", + /// "GB", "GQ", "BQ". Storing all pairs (not just adjacent) enables non-contiguous + /// CamelCase matching like "GQ" matching "GooBarQuux" by skipping "Bar". + /// + private static void AddHumpData(string name, in TemporaryArray charParts, HashSet humpStrings) + { + foreach (var part in charParts) + AddToSet(humpStrings, [char.ToUpperInvariant(name[part.Start])]); + + for (var i = 0; i < charParts.Count; i++) + { + var ci = char.ToUpperInvariant(name[charParts[i].Start]); + for (var j = i + 1; j < charParts.Count; j++) + { + var cj = char.ToUpperInvariant(name[charParts[j].Start]); + AddToSet(humpStrings, [ci, cj]); + } + } + } + + /// + /// Stores lowercased prefixes of each character-part (hump). For "GooBar" with parts + /// "Goo", "Bar": stores "g", "go", "goo", "b", "ba", "bar". Used by the all-lowercase + /// DP to check whether a pattern can be split into segments that each match a hump prefix. + /// + private static void AddHumpPrefixData(in TemporaryArray charParts, ReadOnlySpan loweredName, HashSet humpPrefixStrings) + { + foreach (var part in charParts) + { + for (var prefixLen = 1; prefixLen <= part.Length; prefixLen++) + AddToSet(humpPrefixStrings, loweredName.Slice(part.Start, prefixLen)); + } + } + + /// + /// Stores lowercased trigrams (3-character sliding windows) of each word-part. + /// For "Readline": stores "rea", "ead", "adl", "dli", "lin", "ine". + /// + private static void AddTrigramData(string name, ReadOnlySpan loweredName, HashSet trigramStrings) + { + using var wordParts = TemporaryArray.Empty; + StringBreaker.AddWordParts(name, ref wordParts.AsRef()); + + foreach (var part in wordParts) + { + if (part.Length < 3) + continue; + + for (var i = 0; i + 3 <= part.Length; i++) + AddToSet(trigramStrings, loweredName.Slice(part.Start + i, 3)); + } + } + + private static void AddToSet(HashSet set, ReadOnlySpan value) + { +#if NET9_0_OR_GREATER + set.GetAlternateLookup>().Add(value); +#else + set.Add(value.ToString()); +#endif + } + + private static void AddContainerData(string fullyQualifiedContainerName, HashSet containerChars) + { + if (string.IsNullOrEmpty(fullyQualifiedContainerName)) + return; + + // Break the full container name into character-parts. Dots are treated as punctuation by + // StringBreaker and are naturally skipped, so "System.Collections.Generic" produces parts + // ["System", "Collections", "Generic"] -> stores 'S', 'C', 'G'. + using var charParts = TemporaryArray.Empty; + StringBreaker.AddCharacterParts(fullyQualifiedContainerName, ref charParts.AsRef()); + + foreach (var part in charParts) + containerChars.Add(char.ToUpperInvariant(fullyQualifiedContainerName[part.Start])); + } + + /// + /// Returns a flags value indicating which matching strategies + /// are worth attempting on this document's symbols. Returns + /// if the document definitely does not contain a symbol matching + /// (modulo intentionally unsupported match kinds like + /// ). + /// + public PatternMatcherKind CouldContainNavigateToMatch(string patternName, string? patternContainer) + { + var result = PatternMatcherKind.None; + + if (NonFuzzyCheckPasses(patternName)) + result |= PatternMatcherKind.Standard; + + // Fuzzy matching requires BOTH: (1) a symbol of compatible length exists in the document, + // AND (2) enough of the pattern's bigrams are present. The length check is cheap and fast; + // the bigram check uses the q-gram count lemma to filter more precisely for longer patterns. + if (LengthCheckPasses(patternName) && BigramCountCheckPasses(patternName)) + result |= PatternMatcherKind.Fuzzy; + + if (result == PatternMatcherKind.None) + return PatternMatcherKind.None; + + if (patternContainer != null && !ContainerCheckPasses(patternContainer)) + return PatternMatcherKind.None; + + return result; + } + + /// + /// Checks whether the pattern (after preprocessing) passes hump or trigram checks. Mirrors + /// the 's preprocessing: strips leading non-letter/digit + /// characters (e.g., "@static" → "static") and splits at spaces/asterisks (e.g., "get word" + /// → "get", "word"). Returns only if every effective word passes. + /// Since the requires all words to match, failing fast on the + /// first miss avoids the much more expensive full pattern match later. + /// + private bool NonFuzzyCheckPasses(ReadOnlySpan pattern) + { + if (pattern.Length == 0) + return false; + + // Split at spaces/asterisks and check each word. The PatternMatcher requires ALL words + // to match, so bail on the first failure. For simple patterns without separators, this + // naturally checks the whole pattern once and returns. + while (pattern.Length > 0) + { + var sepIndex = pattern.IndexOfAny(' ', '*'); + if (sepIndex == 0) + { + pattern = pattern[1..]; + continue; + } + + var end = sepIndex < 0 ? pattern.Length : sepIndex; + if (!HumpOrTrigramCheckPasses(pattern[..end])) + return false; + + pattern = pattern[end..]; + } + + // All words passed. + return true; + } + + private bool HumpOrTrigramCheckPasses(ReadOnlySpan pattern) + { + // Strip leading and trailing non-word characters from each sub-word (e.g., "@static" → + // "static", "[class]" → "class") to match how PatternMatcher.PatternSegment extracts + // sub-words. Done here so that each segment after space/asterisk splitting is cleaned + // independently (e.g., "[class] [structure]" → "class", "structure"). + while (pattern.Length > 0 && !PatternMatcher.IsWordChar(pattern[0])) + pattern = pattern[1..]; + + while (pattern.Length > 0 && !PatternMatcher.IsWordChar(pattern[^1])) + pattern = pattern[..^1]; + + if (pattern.Length == 0) + return false; + + var isAllLowercase = IsAllLowercase(pattern); + + // Note: this order is relevant. The trigram check can often return faster than the hump check + // (which needs to do a more expensive DP algorithm). + return TrigramCheckPasses(pattern, isAllLowercase) || HumpCheckPasses(pattern, isAllLowercase); + } + + /// + /// Checks whether the container pattern's hump-initial characters are present in this document. + /// + /// Example: if the document contains a symbol in container System.GooBar.Quux, the + /// stored is { 'S', 'G', 'B', 'Q' } (the uppercase initial + /// of each hump across all dot-separated segments). + /// + /// For a mixed-case pattern like "Go.Ba", we extract hump initials 'G' and 'B', + /// and verify both are in the set → . A pattern like "Go.Xy" + /// extracts 'G' and 'X'; 'X' is not in the set → . + /// + /// For an all-lowercase pattern like "goo", we cannot determine hump boundaries, + /// so we fall back to checking just the first character uppercased: 'G' ∈ set → . + /// + public bool ContainerCheckPasses(ReadOnlySpan patternContainer) + { + if (_containerCharSet == null || patternContainer.Length == 0) + return false; + + if (!IsAllLowercase(patternContainer)) + { + // Mixed-case container pattern (e.g. "Go.Ba"): extract hump initials and check each. + using var charParts = TemporaryArray.Empty; + StringBreaker.AddCharacterParts(patternContainer, ref charParts.AsRef()); + + if (charParts.Count == 0) + return false; + + foreach (var part in charParts) + { + if (!_containerCharSet.Contains(char.ToUpperInvariant(patternContainer[part.Start]))) + return false; + } + + return true; + } + else + { + // All-lowercase container pattern (e.g. "goo"): we can't determine hump boundaries + // without casing, so check just the first character uppercased as a hump initial. + return _containerCharSet.Contains(char.ToUpperInvariant(patternContainer[0])); + } + } + + /// + /// Checks whether the pattern's hump structure is compatible with symbols in this document. + /// + /// For mixed-case patterns (e.g. "GoBa", "GB"), StringBreaker identifies explicit hump + /// boundaries. Single-hump patterns check the individual hump character. Multi-hump patterns + /// check that each adjacent pair of hump initials exists as a stored bigram, which is + /// significantly more selective than checking individual characters (eliminates cross-symbol + /// false positives). + /// + /// For all-lowercase patterns (e.g. "gb", "goba"), a DP checks whether the pattern can + /// be split into segments where each segment is a stored lowercased hump prefix. For example, + /// "goba" can be split as "go"+"ba", matching hump prefixes from "GooBar". This is much more + /// selective than checking a single character: each segment is a multi-character string checked + /// against a large value space. + /// + public bool HumpCheckPasses(ReadOnlySpan patternName) + => HumpCheckPasses(patternName, IsAllLowercase(patternName)); + + private bool HumpCheckPasses(ReadOnlySpan patternName, bool isAllLowercase) + { + if (patternName.Length == 0) + return false; + + return isAllLowercase + ? AllLowercaseHumpCheckPasses(patternName, _humpPrefixFilter) + : MixedCaseHumpCheckPasses(patternName, _humpSet); + } + + /// + /// For an all-lowercase pattern, uses dynamic programming to check whether the pattern can be + /// partitioned into segments where each segment is a lowercased prefix of some hump in the + /// document. For example, "wrli" against "WriteLine" (humps "Write", "Line") succeeds via the + /// split "wr"+"li" — "wr" is a prefix of "write" and "li" is a prefix of "line". + /// + /// Example 1: "getapp" against GetApplicationContext (humps "Get", "Application", "Context"). + /// Stored hump prefixes include "g","ge","get", "a","ap","app",...,"application", "c","co",...,"context". + /// + /// pattern: g e t a p p + /// index: 0 1 2 3 4 5 6 + /// furthest: 0 (start) + /// + /// i=0: "g"✓ "ge"✓ "get"✓ "geta"✗→break furthest: 3 + /// i=1: "e"✗→break furthest: 3 + /// i=2: "t"✗→break furthest: 3 + /// i=3: "a"✓ "ap"✓ "app"✓→j=6=n→done! partition: "get" + "app" ✓ + /// + /// + /// Example 2: "getapp" against GettyTapple (humps "Getty", "Tapple"). + /// Stored hump prefixes include "g","ge","get","gett","getty", "t","ta","tap","tapp","tappl","tapple". + /// A greedy algorithm would commit to "get" (prefix of "getty"), leaving "app" with no match. + /// The DP avoids this by pushing furthest to position 3 via "get", then exploring from + /// position 2 (reachable via "ge") where "tapp" completes the partition: + /// + /// pattern: g e t a p p + /// index: 0 1 2 3 4 5 6 + /// furthest: 0 (start) + /// + /// i=0: "g"✓ "ge"✓ "get"✓ "geta"✗→break furthest: 3 + /// i=1: "e"✗→break furthest: 3 + /// i=2: "t"✓ "ta"✓ "tap"✓ "tapp"✓→j=6=n→done! partition: "ge" + "tapp" ✓ + /// + /// + /// Key optimization: when checking extensions from position i, if pattern[i..j) is not in the + /// filter, we break immediately. No hump can have pattern[i..j+1) as a prefix without also + /// having pattern[i..j) as a prefix, and bloom filters have no false negatives, so the break + /// is safe. This makes the DP run in O(n × max_hump_length) rather than O(n²). + /// + private static bool AllLowercaseHumpCheckPasses(ReadOnlySpan pattern, BloomFilter? humpPrefixFilter) + { + if (humpPrefixFilter == null) + return false; + + var n = pattern.Length; + + // Because the bloom filter stores all prefixes of each hump, the reachable positions always + // form a contiguous range from 0. So we only need to track the furthest reachable index + // rather than a full bool array. + var furthest = 0; + + for (var i = 0; i <= furthest && i < n; i++) + { + for (var j = i + 1; j <= n; j++) + { + if (humpPrefixFilter.ProbablyContains(pattern[i..j])) + { + // A segment ending at the end of the pattern matched — the entire + // pattern can be partitioned into hump prefixes. + if (j == n) + return true; + + // This segment matched a hump prefix but didn't reach the end. + // Extend the frontier so the outer loop will explore from this + // new position — it may lead to a partition we'd miss if we only + // committed to the longest match from earlier positions (see the + // GettyTapple example in the doc comment above). + furthest = Math.Max(furthest, j); + } + else + { + // pattern[i..j) is definitely not a stored hump prefix (bloom filters have + // no false negatives). Therefore no longer extension pattern[i..j+1) can be + // a prefix either, since it would require pattern[i..j) to also be a prefix. + break; + } + } + } + + // The outer loop caught up to the furthest reachable position without ever + // reaching the end of the pattern — no valid partition exists. + return false; + } + + /// + /// For a mixed-case pattern, checks hump-initial characters/bigrams against the hump set. + /// + /// Single-hump patterns (e.g. "Goo") check the individual hump character. + /// Multi-hump patterns (e.g. "GoBa", "GB", "GBQ") check that each adjacent pair of hump + /// initials is a stored bigram. Since the index stores all ordered pairs (not just adjacent) + /// of each symbol's hump initials, this correctly handles non-contiguous matches like "GQ" + /// matching "GooBarQuux". + /// + private static bool MixedCaseHumpCheckPasses(ReadOnlySpan pattern, FrozenSet? humpSet) + { + if (humpSet == null) + return false; + + using var charParts = TemporaryArray.Empty; + StringBreaker.AddCharacterParts(pattern, ref charParts.AsRef()); + + if (charParts.Count == 0) + return false; + + if (charParts.Count == 1) + { + // Single hump: check the individual character. + return ContainsChar(humpSet, char.ToUpperInvariant(pattern[charParts[0].Start])); + } + + // Multi-hump: check that each adjacent pair of pattern hump initials is a stored bigram. + for (var i = 0; i < charParts.Count - 1; i++) + { + var c1 = char.ToUpperInvariant(pattern[charParts[i].Start]); + var c2 = char.ToUpperInvariant(pattern[charParts[i + 1].Start]); + if (!ContainsBigram(humpSet, c1, c2)) + return false; + } + + return true; + } + + /// + /// Checks if all trigrams of the all-lowercase pattern are present in the trigram filter. + /// Only applicable for all-lowercase patterns of length >= 3. + /// + public bool TrigramCheckPasses(ReadOnlySpan pattern) + => TrigramCheckPasses(pattern, IsAllLowercase(pattern)); + + private bool TrigramCheckPasses(ReadOnlySpan pattern, bool isAllLowercase) + { + if (_trigramFilter == null || !isAllLowercase || pattern.Length < 3) + return false; + + for (var i = 0; i + 3 <= pattern.Length; i++) + { + if (!_trigramFilter.ProbablyContains(pattern.Slice(i, 3))) + return false; + } + + return true; + } + + /// + /// Checks if any symbol name length in the document is close enough to the pattern length + /// for a fuzzy match to be possible. The allowed delta is determined by + /// : ±1 for patterns of length 3–5, + /// ±2 for length 6+. Patterns shorter than + /// are rejected outright because + /// disables fuzzy matching for them. + /// + public bool LengthCheckPasses(ReadOnlySpan pattern) + { + if (_symbolNameLengthBitset == 0 || pattern.Length < WordSimilarityChecker.MinFuzzyLength) + return false; + + var threshold = WordSimilarityChecker.GetThreshold(pattern.Length); + + for (var delta = -threshold; delta <= threshold; delta++) + { + var candidateLength = pattern.Length + delta; + if (candidateLength < 0) + continue; + + var bit = Math.Min(candidateLength, MaxSymbolNameLengthBitIndex); + if ((_symbolNameLengthBitset & (1UL << bit)) != 0) + return true; + } + + return false; + } + + /// + /// Maps a character to its index in the -element alphabet used + /// by . Lowercase letters get unique indices (0–25), digits get + /// unique indices (26–35), underscore gets index 36, and all other characters (Unicode letters, + /// etc.) map to a single "other" bucket (37). + /// + private static int FuzzyBigramCharIndex(char c) + => c switch + { + >= 'a' and <= 'z' => c - 'a', + >= '0' and <= '9' => 26 + (c - '0'), + '_' => 36, + _ => 37, + }; + + /// + /// Checks whether enough of the pattern's lowercased bigrams are present in + /// for a fuzzy match to be possible, using the q-gram count + /// lemma (Ukkonen, 1992): if edit_distance(pattern, candidate) <= k, then at least + /// |pattern| - 1 - 2k of the pattern's bigram positions must have a matching bigram in + /// the candidate. (Each edit operation can destroy at most q = 2 bigrams.) + /// + /// Effectiveness by pattern length (k from ): + /// + /// Length 3: k=1, min_shared = 3−1−2 = 0 → cannot filter (always returns true) + /// Length 4: k=1, min_shared = 4−1−2 = 1 → need ≥ 1 of 3 bigrams + /// Length 5: k=1, min_shared = 5−1−2 = 2 → need ≥ 2 of 4 bigrams + /// Length 6: k=2, min_shared = 6−1−4 = 1 → need ≥ 1 of 5 bigrams + /// Length 7: k=2, min_shared = 7−1−4 = 2 → need ≥ 2 of 6 bigrams + /// Length 8+: increasingly strong filtering + /// + /// + /// See: Ukkonen, E. (1992). "Approximate string-matching with q-grams and maximal matches." + /// Theoretical Computer Science, 92(1), 191–211. + /// + /// + public bool BigramCountCheckPasses(ReadOnlySpan pattern) + { + var k = WordSimilarityChecker.GetThreshold(pattern.Length); + var minShared = pattern.Length - 1 - 2 * k; + if (minShared <= 0) + return true; + + var count = 0; + for (var i = 0; i < pattern.Length - 1; i++) + { + var idx = FuzzyBigramCharIndex(char.ToLowerInvariant(pattern[i])) * FuzzyBigramAlphabetSize + + FuzzyBigramCharIndex(char.ToLowerInvariant(pattern[i + 1])); + if ((_fuzzyBigramBitset[idx >> 6] & (1UL << (idx & 63))) != 0) + count++; + + if (count >= minShared) + return true; + } + + return false; + } + + private static bool ContainsChar(FrozenSet set, char c) + { +#if NET9_0_OR_GREATER + return set.GetAlternateLookup>().Contains([c]); +#else + return set.Contains(c.ToString()); +#endif + } + + private static bool ContainsBigram(FrozenSet set, char c1, char c2) + { +#if NET9_0_OR_GREATER + return set.GetAlternateLookup>().Contains([c1, c2]); +#else + return set.Contains(new string([c1, c2])); +#endif + } + + private static bool IsAllLowercase(ReadOnlySpan text) + { + foreach (var c in text) + { + if (char.IsUpper(c)) + return false; + } + + return true; + } + + /// + /// Evaluates a tree against this document's indexed bigrams and trigrams. + /// Returns if the document could plausibly contain a symbol matching the + /// regex, or if the document can be safely skipped. + /// + public bool RegexQueryCheckPasses(PatternMatching.RegexQuery query) + { + switch (query) + { + case PatternMatching.RegexQuery.All all: + foreach (var child in all.Children) + { + if (!RegexQueryCheckPasses(child)) + return false; + } + + return true; + + case PatternMatching.RegexQuery.Any any: + foreach (var child in any.Children) + { + if (RegexQueryCheckPasses(child)) + return true; + } + + return false; + + case PatternMatching.RegexQuery.Literal literal: + return RegexLiteralCheckPasses(literal.Text); + + // The optimizer removes all None nodes from the tree: None children are dropped + // from All (vacuously true), and Any collapses to None if any child is None. If + // the entire tree optimizes to None (no literals at all), Compile returns null and + // the caller never reaches this method. So the only nodes that can appear here are + // All, Any, and Literal — all handled above. + default: + throw ExceptionUtilities.Unreachable(); + } + } + + /// + /// Checks whether the bigrams of a literal string are present in this document's bigram bitset. + /// Uses only bigrams (not trigrams) because the trigram index is segmented by camelCase + /// word-parts, while regex literals are continuous strings that may span word-part boundaries + /// (e.g. "ReadLine" has cross-boundary trigrams "adl"/"dli" that aren't indexed). The bigram + /// bitset covers the full symbol name and is sufficient for filtering. + /// + /// The literal text is expected to already be lowercased at compile time by the regex query + /// compiler. + /// + private bool RegexLiteralCheckPasses(string text) + { + Debug.Assert(text == text.ToLowerInvariant()); + Debug.Assert(text.Length >= 2); + Debug.Assert(!_fuzzyBigramBitset.IsDefault); + + for (var i = 0; i < text.Length - 1; i++) + { + var idx = FuzzyBigramCharIndex(text[i]) * FuzzyBigramAlphabetSize + + FuzzyBigramCharIndex(text[i + 1]); + if ((_fuzzyBigramBitset[idx >> 6] & (1UL << (idx & 63))) == 0) + return false; + } + + return true; + } + + public void WriteTo(ObjectWriter writer) + { + writer.WriteUInt64(_symbolNameLengthBitset); + WriteStringSet(writer, _humpSet); + WriteBloomFilter(writer, _humpPrefixFilter); + WriteBloomFilter(writer, _trigramFilter); + WriteCharSet(writer, _containerCharSet); + WriteBigramBitset(writer, _fuzzyBigramBitset); + } + + private static void WriteStringSet(ObjectWriter writer, FrozenSet set) + { + writer.WriteInt32(set.Count); + foreach (var value in set) + writer.WriteString(value); + } + + private static void WriteCharSet(ObjectWriter writer, FrozenSet set) + { + using var _ = PooledStringBuilder.GetInstance(out var builder); + foreach (var c in set) + builder.Append(c); + + writer.WriteString(builder.ToString()); + } + + private static void WriteBloomFilter(ObjectWriter writer, BloomFilter filter) + => filter.WriteTo(writer); + + public static NavigateToSearchInfo? TryReadFrom(ObjectReader reader) + { + try + { + var lengthBitset = reader.ReadUInt64(); + var humpSet = ReadStringSet(reader); + var humpPrefixFilter = ReadBloomFilter(reader); + var trigramFilter = ReadBloomFilter(reader); + var containerCharSet = ReadCharSet(reader); + var fuzzyBigramBitset = ReadBigramBitset(reader); + return new NavigateToSearchInfo(humpSet, humpPrefixFilter, trigramFilter, containerCharSet, lengthBitset, fuzzyBigramBitset); + } + catch (Exception) + { + } + + return null; + } + + private static FrozenSet ReadStringSet(ObjectReader reader) + { + using var _ = PooledHashSet.GetInstance(out var set); + + var count = reader.ReadInt32(); + for (var i = 0; i < count; i++) + set.Add(reader.ReadString()!); + + return set.ToFrozenSet(); + } + + private static FrozenSet ReadCharSet(ObjectReader reader) + { + var chars = reader.ReadRequiredString(); + return chars.ToFrozenSet(); + } + + private static BloomFilter ReadBloomFilter(ObjectReader reader) + => BloomFilter.ReadFrom(reader); + + private static void WriteBigramBitset(ObjectWriter writer, ImmutableArray bitset) + => writer.WriteArray(bitset, static (writer, value) => writer.WriteUInt64(value)); + + private static ImmutableArray ReadBigramBitset(ObjectReader reader) + => reader.ReadArray(static reader => reader.ReadUInt64()); + } +} diff --git a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex.cs b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex.cs new file mode 100644 index 000000000000..c3cd5613a20e --- /dev/null +++ b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.PatternMatching; +using Microsoft.CodeAnalysis.Storage; + +namespace Microsoft.CodeAnalysis.FindSymbols; + +/// +/// A lightweight index containing only the pre-computed filter data needed for NavigateTo to quickly +/// skip documents that cannot match a search pattern. This is stored and loaded separately from +/// so that the (much larger) declared-symbol data need not be +/// loaded for documents that the filter rejects. +/// +internal sealed partial class NavigateToSearchIndex : AbstractSyntaxIndex +{ + private readonly NavigateToSearchInfo _navigateToSearchInfo; + + private NavigateToSearchIndex( + Checksum? checksum, + NavigateToSearchInfo navigateToSearchInfo) + : base(checksum) + { + _navigateToSearchInfo = navigateToSearchInfo; + } + + /// + /// Returns the flags indicating which matching strategies are + /// worth attempting for this document. Returns if the + /// document can be skipped entirely. Used by NavigateTo to skip documents early. + /// + internal PatternMatcherKind CouldContainNavigateToMatch(string patternName, string? patternContainer) + => _navigateToSearchInfo.CouldContainNavigateToMatch(patternName, patternContainer); + + /// + /// Evaluates a compiled against this document's indexed + /// bigrams to determine if a regex pattern could match any symbol in the document. + /// + public bool RegexQueryCheckPasses(PatternMatching.RegexQuery query) + => _navigateToSearchInfo.RegexQueryCheckPasses(query); + + public static ValueTask GetRequiredIndexAsync(Document document, CancellationToken cancellationToken) + => GetRequiredIndexAsync(SolutionKey.ToSolutionKey(document.Project.Solution), document.Project.State, (DocumentState)document.State, cancellationToken); + + public static ValueTask GetRequiredIndexAsync(SolutionKey solutionKey, ProjectState project, DocumentState document, CancellationToken cancellationToken) + => GetRequiredIndexAsync(solutionKey, project, document, ReadIndex, CreateIndex, cancellationToken); + + public static ValueTask GetIndexAsync(Document document, CancellationToken cancellationToken) + => GetIndexAsync(SolutionKey.ToSolutionKey(document.Project.Solution), document.Project.State, (DocumentState)document.State, cancellationToken); + + public static ValueTask GetIndexAsync(SolutionKey solutionKey, ProjectState project, DocumentState document, CancellationToken cancellationToken) + => GetIndexAsync(solutionKey, project, document, ReadIndex, CreateIndex, cancellationToken); + + public static ValueTask GetIndexAsync(Document document, bool loadOnly, CancellationToken cancellationToken) + => GetIndexAsync(SolutionKey.ToSolutionKey(document.Project.Solution), document.Project.State, (DocumentState)document.State, loadOnly, cancellationToken); + + public static ValueTask GetIndexAsync(SolutionKey solutionKey, ProjectState project, DocumentState document, bool loadOnly, CancellationToken cancellationToken) + => GetIndexAsync(solutionKey, project, document, loadOnly, ReadIndex, CreateIndex, cancellationToken); + + internal TestAccessor GetTestAccessor() + => new(this); + + internal readonly struct TestAccessor(NavigateToSearchIndex index) + { + public bool HumpCheckPasses(string patternName) + => index._navigateToSearchInfo.HumpCheckPasses(patternName); + + public bool TrigramCheckPasses(string patternName) + => index._navigateToSearchInfo.TrigramCheckPasses(patternName); + + public bool LengthCheckPasses(string patternName) + => index._navigateToSearchInfo.LengthCheckPasses(patternName); + + public bool BigramCountCheckPasses(string patternName) + => index._navigateToSearchInfo.BigramCountCheckPasses(patternName); + + public bool ContainerCheckPasses(string patternContainer) + => index._navigateToSearchInfo.ContainerCheckPasses(patternContainer); + + public bool RegexQueryCheckPasses(PatternMatching.RegexQuery query) + => index._navigateToSearchInfo.RegexQueryCheckPasses(query); + + public static NavigateToSearchIndex CreateIndex(ImmutableArray infos) + { + return new NavigateToSearchIndex( + checksum: null, + NavigateToSearchInfo.Create(infos)); + } + } +} diff --git a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex_Create.cs b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex_Create.cs new file mode 100644 index 000000000000..cfc05b8082d1 --- /dev/null +++ b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex_Create.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.FindSymbols; + +internal sealed partial class NavigateToSearchIndex +{ + private static NavigateToSearchIndex CreateIndex( + ProjectState project, SyntaxNode root, Checksum checksum, CancellationToken cancellationToken) + { + var infoFactory = project.LanguageServices.GetRequiredService(); + + using var _1 = ArrayBuilder.GetInstance(out var declaredSymbolInfos); + using var _2 = PooledDictionary>.GetInstance(out var extensionMemberInfo); + try + { + infoFactory.AddDeclaredSymbolInfos( + project, root, declaredSymbolInfos, extensionMemberInfo, cancellationToken); + + return new NavigateToSearchIndex( + checksum, + NavigateToSearchInfo.Create(declaredSymbolInfos)); + } + finally + { + foreach (var (_, builder) in extensionMemberInfo) + builder.Free(); + } + } +} diff --git a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex_Persistence.cs b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex_Persistence.cs new file mode 100644 index 000000000000..0ff80b517dd4 --- /dev/null +++ b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/NavigateToSearchIndex_Persistence.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Storage; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.FindSymbols; + +internal sealed partial class NavigateToSearchIndex +{ + public static Task LoadAsync( + IChecksummedPersistentStorageService storageService, DocumentKey documentKey, Checksum? checksum, StringTable stringTable, CancellationToken cancellationToken) + { + return LoadAsync(storageService, documentKey, checksum, stringTable, ReadIndex, cancellationToken); + } + + public override void WriteTo(ObjectWriter writer) + { + _navigateToSearchInfo.WriteTo(writer); + } + + private static NavigateToSearchIndex? ReadIndex( + StringTable stringTable, ObjectReader reader, Checksum? checksum) + { + var navigateToSearchInfo = NavigateToSearchInfo.TryReadFrom(reader); + + if (navigateToSearchInfo == null) + return null; + + return new NavigateToSearchIndex( + checksum, + navigateToSearchInfo.Value); + } +} diff --git a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex.cs b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex.cs index ec254811aa8f..a19b1213921b 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. diff --git a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex_Create.cs b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex_Create.cs index fd4b89619156..3c4cf9ff5e71 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex_Create.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex_Create.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. diff --git a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex_Persistence.cs b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex_Persistence.cs index 96c0f0d87c7d..6b034ae01aec 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex_Persistence.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/TopLevelSyntaxTree/TopLevelSyntaxTreeIndex_Persistence.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. diff --git a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj index e3a83ddc1970..238ec508d71a 100644 --- a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj +++ b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj @@ -75,7 +75,6 @@ - @@ -121,7 +120,6 @@ - diff --git a/src/Workspaces/Core/Portable/PatternMatching/CompoundPatternMatcher.cs b/src/Workspaces/Core/Portable/PatternMatching/CompoundPatternMatcher.cs new file mode 100644 index 000000000000..ca648dc11a41 --- /dev/null +++ b/src/Workspaces/Core/Portable/PatternMatching/CompoundPatternMatcher.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.PatternMatching; + +internal abstract partial class PatternMatcher +{ + /// + /// A pattern matcher that composes multiple sub-matchers, trying each in order and + /// short-circuiting on the first successful match. + /// + private sealed class CompoundPatternMatcher : PatternMatcher + { + private readonly ArrayBuilder _matchers; + + public CompoundPatternMatcher(ReadOnlySpan matchers) + : base(includeMatchedSpans: false, culture: null) + { + _matchers = ArrayBuilder.GetInstance(matchers.Length); + foreach (var matcher in matchers) + _matchers.Add(matcher); + } + + public override void Dispose() + { + foreach (var matcher in _matchers) + matcher.Dispose(); + + _matchers.Free(); + } + + protected override bool AddMatchesWorker(string candidate, ref TemporaryArray matches) + { + foreach (var matcher in _matchers) + { + if (matcher.AddMatches(candidate, ref matches)) + return true; + } + + return false; + } + } +} diff --git a/src/Workspaces/Core/Portable/PatternMatching/ContainerPatternMatcher.cs b/src/Workspaces/Core/Portable/PatternMatching/ContainerPatternMatcher.cs index 45ed5e473753..d7aa4a4eb204 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/ContainerPatternMatcher.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/ContainerPatternMatcher.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -6,27 +6,31 @@ using System.Globalization; using System.Linq; using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.Shared.Collections; namespace Microsoft.CodeAnalysis.PatternMatching; internal abstract partial class PatternMatcher { + /// + /// Pattern matcher for matching against the container of a symbol (like System.Collections.Generic). Understands + /// how to break on dots and match subportions of that container. Note: all matching is done in a non-fuzzy way. Fuzzy + /// matching is only performed by the . + /// private sealed partial class ContainerPatternMatcher : PatternMatcher { private readonly PatternSegment[] _patternSegments; private readonly char[] _containerSplitCharacters; - public ContainerPatternMatcher( - string[] patternParts, char[] containerSplitCharacters, + internal ContainerPatternMatcher( + string[] patternParts, + char[] containerSplitCharacters, bool includeMatchedSpans, - CultureInfo? culture, - bool allowFuzzyMatching = false) - : base(includeMatchedSpans, culture, allowFuzzyMatching) + CultureInfo? culture) + : base(includeMatchedSpans, culture) { _containerSplitCharacters = containerSplitCharacters; - _patternSegments = [.. patternParts.Select(text => new PatternSegment(text.Trim(), allowFuzzyMatching: allowFuzzyMatching))]; + _patternSegments = [.. patternParts.Select(text => new PatternSegment(text.Trim()))]; _invalidPattern = _patternSegments.Length == 0 || _patternSegments.Any(s => s.IsInvalid); } @@ -41,29 +45,15 @@ public override void Dispose() } } - public override bool AddMatches(string? container, ref TemporaryArray matches) + /// + /// Container matching is always non-fuzzy. + /// + protected override bool AddMatchesWorker(string container, ref TemporaryArray matches) { - if (SkipMatch(container)) - { - return false; - } - - return AddMatches(container, ref matches, fuzzyMatch: false) || - AddMatches(container, ref matches, fuzzyMatch: true); - } - - private bool AddMatches(string container, ref TemporaryArray matches, bool fuzzyMatch) - { - if (fuzzyMatch && !_allowFuzzyMatching) - { - return false; - } - using var tempContainerMatches = TemporaryArray.Empty; var containerParts = container.Split(_containerSplitCharacters, StringSplitOptions.RemoveEmptyEntries); - var relevantDotSeparatedSegmentLength = _patternSegments.Length; if (_patternSegments.Length > containerParts.Length) { // There weren't enough container parts to match against the pattern parts. @@ -79,7 +69,7 @@ private bool AddMatches(string container, ref TemporaryArray match i--, j--) { var containerName = containerParts[j]; - if (!MatchPatternSegment(containerName, ref _patternSegments[i], ref tempContainerMatches.AsRef(), fuzzyMatch)) + if (!MatchPatternSegment(containerName, ref _patternSegments[i], ref tempContainerMatches.AsRef())) { // This container didn't match the pattern piece. So there's no match at all. return false; diff --git a/src/Workspaces/Core/Portable/PatternMatching/FuzzyPatternMatcher.cs b/src/Workspaces/Core/Portable/PatternMatching/FuzzyPatternMatcher.cs new file mode 100644 index 000000000000..6aa623e653fd --- /dev/null +++ b/src/Workspaces/Core/Portable/PatternMatching/FuzzyPatternMatcher.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Shared.Collections; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.PatternMatching; + +internal abstract partial class PatternMatcher +{ + /// + /// Pattern matcher that performs only fuzzy (edit-distance) matching via . Does not attempt any non-fuzzy strategies (exact, prefix, + /// camelCase, substring) — those are handled separately by . + /// Callers compose the two matchers: try first, then fall + /// back to only when non-fuzzy matching fails. + /// + internal sealed class FuzzyPatternMatcher : PatternMatcher + { + private WordSimilarityChecker _similarityChecker; + + public FuzzyPatternMatcher( + string pattern, + bool includeMatchedSpans) + : base(includeMatchedSpans, culture: null) + { + pattern = pattern.Trim(); + + _invalidPattern = string.IsNullOrWhiteSpace(pattern) || pattern.Length < WordSimilarityChecker.MinFuzzyLength; + if (!_invalidPattern) + _similarityChecker = new WordSimilarityChecker(pattern, substringsAreSimilar: false); + } + + public override void Dispose() + { + base.Dispose(); + _similarityChecker.Dispose(); + } + + protected override bool AddMatchesWorker(string candidate, ref TemporaryArray matches) + { + if (_similarityChecker.AreSimilar(candidate)) + { + matches.Add(new PatternMatch( + PatternMatchKind.Fuzzy, punctuationStripped: false, + isCaseSensitive: false, matchedSpan: null)); + return true; + } + + return false; + } + + internal TestAccessor GetTestAccessor() + => new(this); + + internal readonly struct TestAccessor(FuzzyPatternMatcher matcher) + { + public bool LastCacheResultIs(bool areSimilar, string candidateText) + => matcher._similarityChecker.LastCacheResultIs(areSimilar, candidateText); + } + } +} diff --git a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.PatternSegment.cs b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.PatternSegment.cs index 45dc35e5dc1a..0e3cb38ebe7a 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.PatternSegment.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.PatternSegment.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -15,18 +15,18 @@ internal abstract partial class PatternMatcher /// can break the segment into. /// [NonCopyable] - private struct PatternSegment(string text, bool allowFuzzyMatching) : IDisposable + private struct PatternSegment(string text) : IDisposable { // Information about the entire piece of text between the dots. For example, if the // text between the dots is 'Get-Keyword', then TotalTextChunk.Text will be 'Get-Keyword' and // TotalTextChunk.CharacterSpans will correspond to 'G', 'et', 'K' and 'eyword'. - public TextChunk TotalTextChunk = new(text, allowFuzzyMatching); + public TextChunk TotalTextChunk = new(text); // Information about the subwords compromising the total word. For example, if the // text between the dots is 'Get-Keyword', then the subwords will be 'Get' and 'Keyword' // Those individual words will have CharacterSpans of ('G' and 'et') and ('K' and 'eyword') // respectively. - public readonly TextChunk[] SubWordTextChunks = BreakPatternIntoSubWords(text, allowFuzzyMatching); + public readonly TextChunk[] SubWordTextChunks = BreakPatternIntoSubWords(text); public void Dispose() { @@ -68,7 +68,7 @@ private static int CountTextChunks(string pattern) return count; } - private static TextChunk[] BreakPatternIntoSubWords(string pattern, bool allowFuzzyMatching) + private static TextChunk[] BreakPatternIntoSubWords(string pattern) { var partCount = CountTextChunks(pattern); @@ -96,7 +96,7 @@ private static TextChunk[] BreakPatternIntoSubWords(string pattern, bool allowFu { if (wordLength > 0) { - result[resultIndex++] = new TextChunk(pattern.Substring(wordStart, wordLength), allowFuzzyMatching); + result[resultIndex++] = new TextChunk(pattern.Substring(wordStart, wordLength)); wordLength = 0; } } @@ -104,7 +104,7 @@ private static TextChunk[] BreakPatternIntoSubWords(string pattern, bool allowFu if (wordLength > 0) { - result[resultIndex++] = new TextChunk(pattern.Substring(wordStart, wordLength), allowFuzzyMatching); + result[resultIndex++] = new TextChunk(pattern.Substring(wordStart, wordLength)); } return result; diff --git a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.TextChunk.cs b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.TextChunk.cs index 8d51d52bee1d..ffe6ede61128 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.TextChunk.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.TextChunk.cs @@ -1,13 +1,11 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.PatternMatching; @@ -31,24 +29,15 @@ private struct TextChunk : IDisposable /// public TemporaryArray PatternHumps; - /// - /// Not readonly as this value caches data within it, and so it needs to be able to mutate. - /// - public WordSimilarityChecker SimilarityChecker; - public readonly bool IsLowercase; public readonly bool IsUppercase; - public TextChunk(string text, bool allowFuzzingMatching) + public TextChunk(string text) { this.Text = text; PatternHumps = TemporaryArray.Empty; StringBreaker.AddCharacterParts(text, ref PatternHumps); - this.SimilarityChecker = allowFuzzingMatching - ? new WordSimilarityChecker(text, substringsAreSimilar: false) - : default; - IsLowercase = !ContainsUpperCaseLetter(text); IsUppercase = !ContainsLowerCaseLetter(text); } @@ -56,7 +45,6 @@ public TextChunk(string text, bool allowFuzzingMatching) public void Dispose() { this.PatternHumps.Dispose(); - this.SimilarityChecker.Dispose(); } } } diff --git a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.cs b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.cs index f2d253732862..ab573e26119c 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -9,10 +9,8 @@ using System.Globalization; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Shared; -using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.PatternMatching; @@ -32,7 +30,6 @@ internal abstract partial class PatternMatcher : IDisposable public const int CamelCaseMaxWeight = CamelCaseContiguousBonus + CamelCaseMatchesFromStartBonus; private readonly bool _includeMatchedSpans; - private readonly bool _allowFuzzyMatching; // PERF: Cache the culture's compareInfo to avoid the overhead of asking for them repeatedly in inner loops private readonly CompareInfo _compareInfo; @@ -45,11 +42,9 @@ internal abstract partial class PatternMatcher : IDisposable /// /// The culture to use for string searching and comparison. /// Whether or not the matching parts of the candidate should be supplied in results. - /// Whether or not close matches should count as matches. protected PatternMatcher( bool includeMatchedSpans, - CultureInfo? culture, - bool allowFuzzyMatching = false) + CultureInfo? culture) { culture ??= CultureInfo.CurrentCulture; @@ -57,7 +52,6 @@ protected PatternMatcher( _textInfo = culture.TextInfo; _includeMatchedSpans = includeMatchedSpans; - _allowFuzzyMatching = allowFuzzyMatching; } public virtual void Dispose() @@ -66,33 +60,70 @@ public virtual void Dispose() public static PatternMatcher CreatePatternMatcher( string pattern, - CultureInfo? culture = null, - bool includeMatchedSpans = false, - bool allowFuzzyMatching = false) + bool includeMatchedSpans, + PatternMatcherKind kind = PatternMatcherKind.Standard) + { + return CreatePatternMatcher(pattern, culture: null, includeMatchedSpans, kind); + } + + public static PatternMatcher CreatePatternMatcher( + string pattern, + CultureInfo? culture, + bool includeMatchedSpans, + PatternMatcherKind kind = PatternMatcherKind.Standard) { - return new SimplePatternMatcher(pattern, culture, includeMatchedSpans, allowFuzzyMatching); + var standard = kind.HasFlag(PatternMatcherKind.Standard) ? new SimplePatternMatcher(pattern, culture, includeMatchedSpans) : null; + var fuzzy = kind.HasFlag(PatternMatcherKind.Fuzzy) ? new FuzzyPatternMatcher(pattern, includeMatchedSpans) : null; + + return (standard, fuzzy) switch + { + (not null, not null) => new CompoundPatternMatcher([standard, fuzzy]), + (not null, null) => standard, + (null, not null) => fuzzy, + _ => throw new ArgumentException($"{nameof(kind)} must specify at least one matching strategy.", nameof(kind)), + }; } - public static PatternMatcher CreateContainerPatternMatcher( - string[] patternParts, - char[] containerSplitCharacters, - bool includeMatchedSpans = false, - CultureInfo? culture = null, - bool allowFuzzyMatching = false) + [return: NotNullIfNotNull(nameof(pattern))] + public static PatternMatcher? CreateDotSeparatedContainerMatcher( + string? pattern, + bool includeMatchedSpans, + CultureInfo? culture = null) { + if (pattern is null) + return null; + return new ContainerPatternMatcher( - patternParts, containerSplitCharacters, includeMatchedSpans, culture, allowFuzzyMatching); + pattern.Split(s_dotCharacterArray, StringSplitOptions.RemoveEmptyEntries), s_dotCharacterArray, includeMatchedSpans, culture); } - public static PatternMatcher CreateDotSeparatedContainerMatcher( - string pattern, - bool includeMatchedSpans = false, - CultureInfo? culture = null, - bool allowFuzzyMatching = false) + /// + /// Creates a matcher for the "name" portion of a NavigateTo search. When + /// is , returns a regex-based matcher (or if the + /// pattern is not a valid .NET regex). Otherwise returns the standard pattern matcher. + /// + public static PatternMatcher? CreateNameMatcher( + string name, bool isRegex, bool includeMatchedSpans, PatternMatcherKind matchKinds = PatternMatcherKind.Standard) + { + return isRegex + ? RegexPatternMatcher.TryCreate(name, includeMatchedSpans) + : CreatePatternMatcher(name, includeMatchedSpans, matchKinds); + } + + /// + /// Creates a matcher for the "container" portion of a NavigateTo search. When + /// is , returns a regex-based matcher for the container (or + /// if no container or the pattern is invalid). Otherwise returns the standard dot-separated container matcher. + /// + public static PatternMatcher? CreateContainerMatcher( + string? container, bool isRegex, bool includeMatchedSpans) { - return CreateContainerPatternMatcher( - pattern.Split(s_dotCharacterArray, StringSplitOptions.RemoveEmptyEntries), - s_dotCharacterArray, includeMatchedSpans, culture, allowFuzzyMatching); + if (container is null) + return null; + + return isRegex + ? RegexPatternMatcher.TryCreate(container, includeMatchedSpans) + : CreateDotSeparatedContainerMatcher(container, includeMatchedSpans); } internal static (string name, string? containerOpt) GetNameAndContainer(string pattern) @@ -104,7 +135,15 @@ internal static (string name, string? containerOpt) GetNameAndContainer(string p : (name: pattern, containerOpt: null); } - public abstract bool AddMatches(string? candidate, ref TemporaryArray matches); + public bool AddMatches(string? candidate, ref TemporaryArray matches) + { + if (SkipMatch(candidate)) + return false; + + return AddMatchesWorker(candidate, ref matches); + } + + protected abstract bool AddMatchesWorker(string candidate, ref TemporaryArray matches); private bool SkipMatch([NotNullWhen(false)] string? candidate) => _invalidPattern || string.IsNullOrWhiteSpace(candidate); @@ -134,32 +173,6 @@ private static bool ContainsLowerCaseLetter(string pattern) } private PatternMatch? MatchPatternChunk( - string candidate, - ref TextChunk patternChunk, - bool punctuationStripped, - bool fuzzyMatch) - { - return fuzzyMatch - ? FuzzyMatchPatternChunk(candidate, ref patternChunk, punctuationStripped) - : NonFuzzyMatchPatternChunk(candidate, patternChunk, punctuationStripped); - } - - private static PatternMatch? FuzzyMatchPatternChunk( - string candidate, - ref TextChunk patternChunk, - bool punctuationStripped) - { - Contract.ThrowIfTrue(patternChunk.SimilarityChecker.IsDefault); - if (patternChunk.SimilarityChecker.AreSimilar(candidate)) - { - return new PatternMatch( - PatternMatchKind.Fuzzy, punctuationStripped, isCaseSensitive: false, matchedSpan: null); - } - - return null; - } - - private PatternMatch? NonFuzzyMatchPatternChunk( string candidate, in TextChunk patternChunk, bool punctuationStripped) @@ -333,19 +346,11 @@ private static bool ContainsSpaceOrAsterisk(string text) /// The word being tested. /// The segment of the pattern to check against the candidate. /// The result array to place the matches in. - /// If a fuzzy match should be performed - /// If there's only one match, then the return value is that match. Otherwise it is null. private bool MatchPatternSegment( string candidate, ref PatternSegment segment, - ref TemporaryArray matches, - bool fuzzyMatch) + ref TemporaryArray matches) { - if (fuzzyMatch && !_allowFuzzyMatching) - { - return false; - } - // First check if the segment matches as is. This is also useful if the segment contains // characters we would normally strip when splitting into parts that we also may want to // match in the candidate. For example if the segment is "@int" and the candidate is @@ -356,7 +361,7 @@ private bool MatchPatternSegment( if (!ContainsSpaceOrAsterisk(segment.TotalTextChunk.Text)) { var match = MatchPatternChunk( - candidate, ref segment.TotalTextChunk, punctuationStripped: false, fuzzyMatch: fuzzyMatch); + candidate, segment.TotalTextChunk, punctuationStripped: false); if (match != null) { matches.Add(match.Value); @@ -372,7 +377,7 @@ private bool MatchPatternSegment( // // 2) For each word try to match the word against the candidate value. // - // 3) Matching logic is outlined in NonFuzzyMatchPatternChunk. It's not repeated here to + // 3) Matching logic is outlined in MatchPatternChunk. It's not repeated here to // prevent having multiple places to keep up to date. // // Only if all words have some sort of match is the pattern considered matched. @@ -384,11 +389,9 @@ private bool MatchPatternSegment( if (subWordTextChunks.Length == 1) { var result = MatchPatternChunk( - candidate, ref subWordTextChunks[0], punctuationStripped: true, fuzzyMatch: fuzzyMatch); + candidate, subWordTextChunks[0], punctuationStripped: true); if (result == null) - { return false; - } matches.Add(result.Value); return true; @@ -399,9 +402,8 @@ private bool MatchPatternSegment( for (int i = 0, n = subWordTextChunks.Length; i < n; i++) { - // Try to match the candidate with this word var result = MatchPatternChunk( - candidate, ref subWordTextChunks[i], punctuationStripped: true, fuzzyMatch: fuzzyMatch); + candidate, subWordTextChunks[i], punctuationStripped: true); if (result == null) return false; @@ -413,7 +415,7 @@ private bool MatchPatternSegment( } } - private static bool IsWordChar(char ch) + internal static bool IsWordChar(char ch) => char.IsLetterOrDigit(ch) || ch == '_'; /// diff --git a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcherExtensions.cs b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcherExtensions.cs index cb4d669c21cf..1573bb02a1d8 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcherExtensions.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcherExtensions.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis.Collections; -using Microsoft.CodeAnalysis.Shared.Collections; namespace Microsoft.CodeAnalysis.PatternMatching; diff --git a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcherKind.cs b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcherKind.cs new file mode 100644 index 000000000000..1a18536daa7b --- /dev/null +++ b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcherKind.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.CodeAnalysis.PatternMatching; + +[Flags] +internal enum PatternMatcherKind +{ + None = 0, + Standard = 1, + Fuzzy = 2, +} diff --git a/src/Workspaces/Core/Portable/PatternMatching/RegexPatternMatcher.cs b/src/Workspaces/Core/Portable/PatternMatching/RegexPatternMatcher.cs new file mode 100644 index 000000000000..66f4934cba51 --- /dev/null +++ b/src/Workspaces/Core/Portable/PatternMatching/RegexPatternMatcher.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.PatternMatching; + +internal abstract partial class PatternMatcher +{ + /// + /// A pattern matcher that uses a compiled for matching. Performs + /// case-insensitive finding (so "readline" matches "ReadLine"), but categorizes matches + /// as case-sensitive when the case-sensitive regex also matches. This is consistent with + /// how standard NavigateTo works: find broadly, then rank case-sensitive matches higher. + /// + private sealed class RegexPatternMatcher( + Regex caseInsensitiveRegex, Regex caseSensitiveRegex, bool includeMatchedSpans, CultureInfo? culture) + : PatternMatcher(includeMatchedSpans, culture) + { + /// + /// Tries to create a for the given pattern. Returns + /// if the pattern is not a valid .NET regex (e.g. unclosed groups, + /// invalid escape sequences). Callers should fall back to the standard search path. + /// + public static RegexPatternMatcher? TryCreate(string pattern, bool includeMatchedSpans, CultureInfo? culture = null) + { + try + { + // IgnorePatternWhitespace lets users write readable patterns like `( Read | Write ) Line` + // without needing manual stripping. Symbol names never contain whitespace, so this is safe. + // + // Both regexes are compiled to native code on first use, amortizing the compilation + // cost across the many candidate strings checked during a single NavigateTo search. + const RegexOptions commonOptions = RegexOptions.CultureInvariant | RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace; + + // Ensure that regexes that run too long (e.g. due to catastrophic backtracking) always terminate. + var timeout = TimeSpan.FromSeconds(1); + + var caseInsensitive = new Regex(pattern, commonOptions | RegexOptions.IgnoreCase, timeout); + var caseSensitive = new Regex(pattern, commonOptions, timeout); + return new RegexPatternMatcher(caseInsensitive, caseSensitive, includeMatchedSpans, culture); + } + catch (ArgumentException) + { + return null; + } + } + + protected override bool AddMatchesWorker(string candidate, ref TemporaryArray matches) + { + var caseInsensitiveMatch = caseInsensitiveRegex.Match(candidate); + if (!caseInsensitiveMatch.Success) + return false; + + // Run the case-sensitive regex to determine categorization. The UI ranks + // case-sensitive matches higher, so we report it when both match. + var caseSensitiveMatch = caseSensitiveRegex.Match(candidate); + var isCaseSensitive = caseSensitiveMatch.Success; + + var bestMatch = isCaseSensitive ? caseSensitiveMatch : caseInsensitiveMatch; + + // Regex matching intentionally uses a simplified two-tier kind system (Exact vs + // NonLowercaseSubstring) rather than the full CamelCase/Prefix/Substring hierarchy. + // The standard hierarchy relies on word-boundary analysis that doesn't apply to + // arbitrary regex matches. + var kind = bestMatch.Length == candidate.Length && bestMatch.Index == 0 + ? PatternMatchKind.Exact + : PatternMatchKind.NonLowercaseSubstring; + + var matchedSpan = GetMatchedSpan(bestMatch.Index, bestMatch.Length); + + matches.Add(new PatternMatch(kind, punctuationStripped: false, isCaseSensitive, matchedSpan)); + return true; + } + } +} diff --git a/src/Workspaces/Core/Portable/PatternMatching/RegexQuery.cs b/src/Workspaces/Core/Portable/PatternMatching/RegexQuery.cs new file mode 100644 index 000000000000..21a3b3bb3b53 --- /dev/null +++ b/src/Workspaces/Core/Portable/PatternMatching/RegexQuery.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.CodeAnalysis.PatternMatching; + +/// +/// A boolean query tree compiled from a regex AST, used to pre-filter documents before running +/// the full (expensive) regex match. Each node evaluates against a document's indexed +/// bigrams/trigrams to quickly reject documents that cannot possibly contain a match. +/// +/// The tree mirrors the boolean structure of the regex: concatenation becomes +/// (AND), alternation becomes (OR), and opaque constructs (wildcards, character +/// classes) become (passthrough). A pattern like (Read|Write)Line compiles +/// to All(Any(Literal("Read"), Literal("Write")), Literal("Line")), requiring "Line"'s +/// bigrams to be present and at least one of "Read" or "Write"'s bigrams. +/// +/// When the entire tree reduces to (e.g. for .* which has no extractable +/// literals), is and callers skip pre-filtering — +/// every document becomes a candidate and must be checked with the full regex. +/// +internal abstract class RegexQuery +{ + private RegexQuery() { } + + /// + /// Whether this tree contains any nodes whose bigrams/trigrams + /// can be checked against the document index. When , the query + /// cannot reject any documents and pre-filtering should be skipped entirely. + /// + public abstract bool HasLiterals { get; } + + /// + /// Conjunction: all children must be satisfied. Produced from regex concatenation + /// (AB = A followed by B). + /// + public sealed class All(ImmutableArray children) : RegexQuery + { + public readonly ImmutableArray Children = children; + public override bool HasLiterals => Children.Any(static c => c.HasLiterals); + } + + /// + /// Disjunction: at least one child must be satisfied. Produced from regex alternation + /// (A|B). + /// + public sealed class Any(ImmutableArray children) : RegexQuery + { + public readonly ImmutableArray Children = children; + public override bool HasLiterals => Children.Any(static c => c.HasLiterals); + } + + /// + /// A literal string that must appear somewhere in the document's symbol names. + /// At pre-filter time, the literal's lowercased bigrams and trigrams are checked + /// against the document's indexed bitset and Bloom filter. The text is always + /// lowercase and at least two characters long, guaranteeing that every literal + /// contributes at least one bigram to the pre-filter check. + /// + public sealed class Literal : RegexQuery + { + public readonly string Text; + public override bool HasLiterals => true; + + public Literal(string text) + { + Debug.Assert(text.Length >= 2); + Debug.Assert(text == text.ToLowerInvariant()); + Text = text; + } + } + + /// + /// An opaque node that cannot contribute to pre-filtering (e.g. ., \d, + /// character classes). Always evaluates to in the pre-filter, + /// meaning "I can't tell — don't reject on my account." + /// + public sealed class None : RegexQuery + { + public static readonly None Instance = new(); + private None() { } + public override bool HasLiterals => false; + } + + /// + /// Simplifies the query tree by flattening nested / nodes, + /// removing where safe, and collapsing single-child wrappers. + /// + /// The key asymmetry: means "anything could match here." In an + /// (AND), that's vacuously true and can be dropped — the remaining children + /// still constrain the match. In an (OR), it poisons the whole disjunction — + /// if one branch can match anything, the entire can match anything, so it + /// collapses to . + /// + /// Post-condition: The returned tree contains only as the top-level + /// result (meaning the entire regex is opaque). If the result is an , + /// , or , no nodes exist anywhere + /// in the subtree. This allows consumers to assume only those three types appear when + /// traversing a non-None result. + /// + public static RegexQuery Optimize(RegexQuery query) + { + return query switch + { + All all => OptimizeAll(all), + Any any => OptimizeAny(any), + _ => query, + }; + + static RegexQuery OptimizeAll(All all) + { + using var _ = Microsoft.CodeAnalysis.PooledObjects.ArrayBuilder.GetInstance(out var children); + + foreach (var child in all.Children) + { + var optimized = Optimize(child); + + // None in an All is vacuously true — drop it. For example, in the regex `Goo.*Bar`, + // `.*` compiles to None (matches anything). The All becomes All(Literal("Goo"), None, + // Literal("Bar")). Since we AND all children, None (= "anything could be here") doesn't + // restrict the match, so dropping it yields All(Literal("Goo"), Literal("Bar")), which + // correctly requires both "Goo" and "Bar" bigrams to be present. + if (optimized is None) + continue; + + // Flatten nested All: All(All(a, b), c) -> All(a, b, c). + if (optimized is All inner) + children.AddRange(inner.Children); + else + children.Add(optimized); + } + + return children.Count switch + { + 0 => None.Instance, + 1 => children[0], + _ => new All(children.ToImmutable()), + }; + } + + static RegexQuery OptimizeAny(Any any) + { + using var _ = Microsoft.CodeAnalysis.PooledObjects.ArrayBuilder.GetInstance(out var children); + + foreach (var child in any.Children) + { + var optimized = Optimize(child); + + // None in an Any means "anything could match" — the whole Any is opaque. + if (optimized is None) + return None.Instance; + + // Flatten nested Any: Any(Any(a, b), c) -> Any(a, b, c). + if (optimized is Any inner) + children.AddRange(inner.Children); + else + children.Add(optimized); + } + + return children.Count switch + { + 0 => None.Instance, + 1 => children[0], + _ => new Any(children.ToImmutable()), + }; + } + } +} diff --git a/src/Workspaces/Core/Portable/PatternMatching/SimplePatternMatcher.cs b/src/Workspaces/Core/Portable/PatternMatching/SimplePatternMatcher.cs index df6028079c65..c00f85116955 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/SimplePatternMatcher.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/SimplePatternMatcher.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -19,13 +19,12 @@ internal sealed partial class SimplePatternMatcher : PatternMatcher public SimplePatternMatcher( string pattern, CultureInfo culture, - bool includeMatchedSpans, - bool allowFuzzyMatching) - : base(includeMatchedSpans, culture, allowFuzzyMatching) + bool includeMatchedSpans) + : base(includeMatchedSpans, culture) { pattern = pattern.Trim(); - _fullPatternSegment = new PatternSegment(pattern, allowFuzzyMatching); + _fullPatternSegment = new PatternSegment(pattern); _invalidPattern = _fullPatternSegment.IsInvalid; } @@ -41,24 +40,8 @@ public override void Dispose() /// /// If this was a match, a set of match types that occurred while matching the /// patterns. If it was not a match, it returns null. - public override bool AddMatches(string candidate, ref TemporaryArray matches) - { - if (SkipMatch(candidate)) - { - return false; - } - - return MatchPatternSegment(candidate, ref _fullPatternSegment, ref matches, fuzzyMatch: false) || - MatchPatternSegment(candidate, ref _fullPatternSegment, ref matches, fuzzyMatch: true); - } + protected override bool AddMatchesWorker(string candidate, ref TemporaryArray matches) + => MatchPatternSegment(candidate, ref _fullPatternSegment, ref matches); - public TestAccessor GetTestAccessor() - => new(this); - - public readonly struct TestAccessor(SimplePatternMatcher simplePatternMatcher) - { - public readonly bool LastCacheResultIs(bool areSimilar, string candidateText) - => simplePatternMatcher._fullPatternSegment.TotalTextChunk.SimilarityChecker.LastCacheResultIs(areSimilar, candidateText); - } } } diff --git a/src/Workspaces/Core/Portable/Rename/IRemoteRenamerService.cs b/src/Workspaces/Core/Portable/Rename/IRemoteRenamerService.cs index 1b4348b70c32..f0984a8ae0db 100644 --- a/src/Workspaces/Core/Portable/Rename/IRemoteRenamerService.cs +++ b/src/Workspaces/Core/Portable/Rename/IRemoteRenamerService.cs @@ -136,7 +136,7 @@ internal sealed partial class SymbolicRenameLocations serializableLocations.Options, locations, implicitLocations, - referencedSymbols); + referencedSymbols!); } } diff --git a/src/Workspaces/Core/Portable/Rename/LightweightRenameLocations.cs b/src/Workspaces/Core/Portable/Rename/LightweightRenameLocations.cs index ad9b98cce7fd..8891be76f917 100644 --- a/src/Workspaces/Core/Portable/Rename/LightweightRenameLocations.cs +++ b/src/Workspaces/Core/Portable/Rename/LightweightRenameLocations.cs @@ -63,7 +63,7 @@ private LightweightRenameLocations( Options, Locations, implicitLocations, - referencedSymbols); + referencedSymbols!); } /// diff --git a/src/Workspaces/Core/Portable/Rename/RenameUtilities.cs b/src/Workspaces/Core/Portable/Rename/RenameUtilities.cs index 61184287bb52..47b16cd46cc9 100644 --- a/src/Workspaces/Core/Portable/Rename/RenameUtilities.cs +++ b/src/Workspaces/Core/Portable/Rename/RenameUtilities.cs @@ -337,10 +337,25 @@ public static Regex GetRegexForMatch(string matchText) public static async Task TryGetRenamableSymbolAsync( Document document, int position, CancellationToken cancellationToken) { - var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken: cancellationToken).ConfigureAwait(false); + var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var semanticInfo = await SymbolFinder.GetSemanticInfoAtPositionAsync( + semanticModel, position, document.Project.Solution.Services, cancellationToken).ConfigureAwait(false); + + var symbol = semanticInfo.GetAnySymbol(includeType: false); + if (symbol == null) return null; + // For conversion operators, GetSemanticInfo returns the conversion operator as DeclaredSymbol when the + // position is on the return type (since the operator's location is set to the return type location). + // But for rename, we want to rename the type, not the operator. Check if we have a conversion operator + // as the declared symbol AND we have a type in the referenced symbols - if so, prefer the type. + if (symbol is IMethodSymbol { MethodKind: MethodKind.Conversion } && + semanticInfo.ReferencedSymbols.FirstOrDefault() is INamedTypeSymbol referencedType) + { + symbol = referencedType; + } + var definitionSymbol = await FindDefinitionSymbolAsync(symbol, document.Project.Solution, cancellationToken).ConfigureAwait(false); Contract.ThrowIfNull(definitionSymbol); diff --git a/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs b/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs index 2d43a74f992f..54654c613dca 100644 --- a/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs +++ b/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -115,6 +115,9 @@ private static int ComputeK(int expectedCount, double falsePositiveProbability) /// Murmur hash is public domain. Actual code is included below as reference. /// private static int ComputeHash(string key, int seed, bool isCaseSensitive) + => ComputeHash(key.AsSpan(), seed, isCaseSensitive); + + private static int ComputeHash(ReadOnlySpan key, int seed, bool isCaseSensitive) { unchecked { @@ -226,7 +229,7 @@ private static uint CombineTwoCharacters(uint h, uint c1, uint c2) } } - private static char GetCharacter(string key, int index, bool isCaseSensitive) + private static char GetCharacter(ReadOnlySpan key, int index, bool isCaseSensitive) { var c = key[index]; return isCaseSensitive ? c : char.ToLowerInvariant(c); @@ -317,6 +320,12 @@ private void AddRange(HashSet values) } public void Add(string value) + => Add(value.AsSpan()); + + public void Add(char value) + => Add([value]); + + public void Add(ReadOnlySpan value) { for (var i = 0; i < _hashFunctionCount; i++) { @@ -361,6 +370,28 @@ public bool ProbablyContains(string value) return true; } + public bool ProbablyContains(char value) + => ProbablyContains([value]); + + /// + /// Not shared with the overload because that version + /// caches computed hashes on the heap via , which is a significant + /// win when the same string is checked repeatedly across many bloom filters. + /// + public bool ProbablyContains(ReadOnlySpan value) + { + for (var i = 0; i < _hashFunctionCount; i++) + { + var hash = ComputeHash(value, i, _isCaseSensitive); + if (!_bitArray[GetBitArrayIndexFromHash(hash)]) + { + return false; + } + } + + return true; + } + public bool ProbablyContains(long value) { for (var i = 0; i < _hashFunctionCount; i++) diff --git a/src/Workspaces/Core/Portable/Workspace/Host/Metadata/IMetadataService.cs b/src/Workspaces/Core/Portable/Workspace/Host/Metadata/IMetadataService.cs index cfc670a49195..7600d6d945c7 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/Metadata/IMetadataService.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/Metadata/IMetadataService.cs @@ -4,7 +4,14 @@ namespace Microsoft.CodeAnalysis.Host; +/// +/// Provides metadata references for files on disk. +/// internal interface IMetadataService : IWorkspaceService { + /// + /// Returns a reference backed by the file at . + /// If the file does not exist a reference is still returned but reading it will throw. + /// PortableExecutableReference GetReference(string resolvedPath, MetadataReferenceProperties properties); } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/Metadata/MetadataServiceFactory.cs b/src/Workspaces/Core/Portable/Workspace/Host/Metadata/MetadataServiceFactory.cs index 9a2fec57b151..7f87d60fac45 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/Metadata/MetadataServiceFactory.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/Metadata/MetadataServiceFactory.cs @@ -2,32 +2,52 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Composition; +using System.IO; using Microsoft.CodeAnalysis.Host.Mef; namespace Microsoft.CodeAnalysis.Host; [ExportWorkspaceServiceFactory(typeof(IMetadataService), ServiceLayer.Default), Shared] -internal sealed class MetadataServiceFactory : IWorkspaceServiceFactory +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class MetadataServiceFactory() : IWorkspaceServiceFactory { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public MetadataServiceFactory() - { - } - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new DefaultMetadataService(workspaceServices.GetService()); + => new MetadataService(workspaceServices.GetRequiredService()); - private sealed class DefaultMetadataService(IDocumentationProviderService documentationService) : IMetadataService + internal sealed class MetadataService(IDocumentationProviderService documentationProviderService) : IMetadataService { private readonly MetadataReferenceCache _metadataCache = new((path, properties) => - MetadataReference.CreateFromFile(path, properties, documentationService.GetDocumentationProvider(path))); + { + var documentationProvider = documentationProviderService.GetDocumentationProvider(path); + + try + { + return MetadataReference.CreateFromFile(path, properties, documentationProvider); + } + catch (IOException e) + { + // Store failed references in the cache so that the behavior stays consistent once we observe the failure. + return new ThrowingExecutableReference(path, properties, documentationProvider, e); + } + }); public PortableExecutableReference GetReference(string resolvedPath, MetadataReferenceProperties properties) => (PortableExecutableReference)_metadataCache.GetReference(resolvedPath, properties); + + private sealed class ThrowingExecutableReference(string resolvedPath, MetadataReferenceProperties properties, DocumentationProvider documentationProvider, IOException exception) + : PortableExecutableReference(properties, resolvedPath) + { + protected override DocumentationProvider CreateDocumentationProvider() + => documentationProvider; + + protected override Metadata GetMetadataImpl() + => throw exception; + + protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties) + => new ThrowingExecutableReference(FilePath!, properties, documentationProvider, exception); + } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/SolutionServices.cs b/src/Workspaces/Core/Portable/Workspace/Host/SolutionServices.cs index 411d47e8f467..cf73ccddfa85 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/SolutionServices.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/SolutionServices.cs @@ -66,4 +66,10 @@ internal HostLanguageServices GetExtendedLanguageServices(string language) internal IEnumerable FindLanguageServices(HostWorkspaceServices.MetadataFilter filter) => _services.FindLanguageServices(filter); + + /// + /// Returns languages that support the given language service. + /// + internal ImmutableArray GetSupportedLanguages() where TLanguageService : ILanguageService + => SupportedLanguagesArray.WhereAsArray(language => GetLanguageServices(language).GetService() != null); } diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs index 14578055e8ff..7c6857d95d53 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs @@ -1362,8 +1362,14 @@ public void RemoveProjectReference(ProjectReference projectReference) #endregion public void RemoveFromWorkspace() + => RemoveFromWorkspaceMaybeAsync(useAsync: false).VerifyCompleted(); + + public ValueTask RemoveFromWorkspaceAsync() + => RemoveFromWorkspaceMaybeAsync(useAsync: true); + + private async ValueTask RemoveFromWorkspaceMaybeAsync(bool useAsync) { - using (_gate.DisposableWait()) + using (useAsync ? await _gate.DisposableWaitAsync().ConfigureAwait(false) : _gate.DisposableWait()) { if (!_projectSystemProjectFactory.Workspace.CurrentSolution.ContainsProject(Id)) { @@ -1383,21 +1389,25 @@ public void RemoveFromWorkspace() _documentFileChangeContext.Dispose(); - IReadOnlyList? originalMetadataReferences = null; - IReadOnlyList? originalAnalyzerReferences = null; + IReadOnlyList? remainingMetadataReferences = null; + IReadOnlyList? remainingAnalyzerReferences = null; - _projectSystemProjectFactory.ApplyChangeToWorkspace(w => + await _projectSystemProjectFactory.ApplyChangeToWorkspaceMaybeAsync(useAsync, w => { // Acquire the remaining metadata references inside the workspace lock. This is critical // as another project being removed at the same time could result in project to project // references being converted to metadata references (or vice versa) and we might either // miss stopping a file watcher or might end up double-stopping a file watcher. - var project = w.CurrentSolution.GetRequiredProject(Id); + // + // It's also critical we remove ourselves from all our tracking first. There is an edge case where + // if we have a metadata reference to ourselves, then removing us might try to convert that reference + // to a project reference to some other project. + _projectSystemProjectFactory.RemoveProjectFromTrackingMaps_NoLock(Id); - originalMetadataReferences = project.MetadataReferences; - originalAnalyzerReferences = project.AnalyzerReferences; + var project = w.CurrentSolution.GetRequiredProject(Id); - _projectSystemProjectFactory.RemoveProjectFromTrackingMaps_NoLock(Id); + remainingMetadataReferences = project.MetadataReferences; + remainingAnalyzerReferences = project.AnalyzerReferences; // If this is our last project, clear the entire solution. if (w.CurrentSolution.ProjectIds.Count == 1) @@ -1408,15 +1418,15 @@ public void RemoveFromWorkspace() { _projectSystemProjectFactory.Workspace.OnProjectRemoved(Id); } - }); + }).ConfigureAwait(false); - Contract.ThrowIfNull(originalMetadataReferences); - Contract.ThrowIfNull(originalAnalyzerReferences); + Contract.ThrowIfNull(remainingMetadataReferences); + Contract.ThrowIfNull(remainingAnalyzerReferences); - foreach (var reference in originalMetadataReferences.OfType()) + foreach (var reference in remainingMetadataReferences.OfType()) _projectSystemProjectFactory.FileWatchedPortableExecutableReferenceFactory.StopWatchingReference(reference.FilePath!, referenceToTrack: reference); - foreach (var reference in originalAnalyzerReferences) + foreach (var reference in remainingAnalyzerReferences) _projectSystemProjectFactory.FileWatchedAnalyzerReferenceFactory.StopWatchingReference(reference.FullPath!, referenceToTrack: reference); } diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs index 01b1812703c9..474dad4ae524 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs @@ -97,23 +97,11 @@ public ProjectSystemProjectFactory( public FileTextLoader CreateFileTextLoader(string fullPath) => new WorkspaceFileTextLoader(this.SolutionServices, fullPath, defaultEncoding: null); - public async Task CreateAndAddToWorkspaceAsync(string projectSystemName, string language, ProjectSystemProjectCreationInfo creationInfo, ProjectSystemHostInfo hostInfo) + public async Task CreateAndAddToWorkspaceAsync(string projectSystemName, string language, ProjectSystemProjectCreationInfo creationInfo, ProjectSystemHostInfo hostInfo, CancellationToken cancellationToken = default) { var projectId = ProjectId.CreateNewId(projectSystemName); var assemblyName = creationInfo.AssemblyName ?? projectSystemName; - // We will use the project system name as the default display name of the project - var project = new ProjectSystemProject( - this, - hostInfo, - projectId, - displayName: projectSystemName, - language, - assemblyName, - creationInfo.CompilationOptions, - creationInfo.FilePath, - creationInfo.ParseOptions); - var versionStamp = creationInfo.FilePath != null ? VersionStamp.Create(File.GetLastWriteTimeUtc(creationInfo.FilePath)) : VersionStamp.Create(); @@ -131,7 +119,8 @@ public async Task CreateAndAddToWorkspaceAsync(string proj outputFilePath: creationInfo.CompilationOutputAssemblyFilePath, filePath: creationInfo.FilePath, telemetryId: creationInfo.TelemetryId, - hasSdkCodeStyleAnalyzers: project.HasSdkCodeStyleAnalyzers), + // Since we don't have any analyzers at this point, we can just set to false. + hasSdkCodeStyleAnalyzers: false), compilationOptions: creationInfo.CompilationOptions, parseOptions: creationInfo.ParseOptions); @@ -174,10 +163,31 @@ await ApplyChangeToWorkspaceAsync(w => }, onBeforeUpdate: null, onAfterUpdate: null); - }).ConfigureAwait(false); + + // We have now created the project and added it to the solution -- we are committed at this point + // to returning a project or else we would never have a way to remove this project we created. + cancellationToken = CancellationToken.None; + + _projectUpdateState = _projectUpdateState with + { + ProjectReferenceInfos = _projectUpdateState.ProjectReferenceInfos.Add(projectId, new ProjectReferenceInformation([], [])) + }; + }, cancellationToken).ConfigureAwait(false); CodeAnalysisEventSource.Log.ProjectCreated(projectSystemName, creationInfo.FilePath); + // We will use the project system name as the default display name of the project + var project = new ProjectSystemProject( + this, + hostInfo, + projectId, + displayName: projectSystemName, + language, + assemblyName, + creationInfo.CompilationOptions, + creationInfo.FilePath, + creationInfo.ParseOptions); + // Set this value early after solution is created so it is available to Razor. This will get updated // when the command line is set, but we want a non-null value to be available as soon as possible. // @@ -359,21 +369,15 @@ private void ApplyBatchChangeToWorkspace_NoLock( ApplyBatchChangeToWorkspaceMaybe_NoLockAsync(useAsync: false, mutation, onAfterUpdateAlways).VerifyCompleted(); } - private static ProjectUpdateState GetReferenceInformation(ProjectId projectId, ProjectUpdateState projectUpdateState, out ProjectReferenceInformation projectReference) + private static bool TryGetReferenceInformation(ProjectId projectId, ProjectUpdateState projectUpdateState, out ProjectReferenceInformation projectReference) { - if (projectUpdateState.ProjectReferenceInfos.TryGetValue(projectId, out var referenceInfo)) - { - projectReference = referenceInfo; - return projectUpdateState; - } - else - { - projectReference = new ProjectReferenceInformation([], []); - return projectUpdateState with - { - ProjectReferenceInfos = projectUpdateState.ProjectReferenceInfos.Add(projectId, projectReference) - }; - } + return projectUpdateState.ProjectReferenceInfos.TryGetValue(projectId, out projectReference); + } + + private static ProjectReferenceInformation GetRequiredReferenceInformation(ProjectId projectId, ProjectUpdateState projectUpdateState) + { + Contract.ThrowIfFalse(projectUpdateState.ProjectReferenceInfos.TryGetValue(projectId, out var referenceInfo), $"Expected ProjectReferenceInfos entry for project '{projectId}'"); + return referenceInfo; } /// @@ -496,7 +500,7 @@ public static ProjectUpdateState AddProjectOutputPath_NoLock( ProjectUpdateState projectUpdateState, SolutionServices solutionServices) { - projectUpdateState = GetReferenceInformation(projectId, projectUpdateState, out var projectReferenceInformation); + var projectReferenceInformation = GetRequiredReferenceInformation(projectId, projectUpdateState); projectUpdateState = projectUpdateState.WithProjectReferenceInfo(projectId, projectReferenceInformation with { OutputPaths = projectReferenceInformation.OutputPaths.Add(outputPath) @@ -562,6 +566,7 @@ private static ProjectUpdateState ConvertMetadataReferencesToProjectReferences_N // but metadata references come in later. CanConvertMetadataReferenceToProjectReference isn't terribly expensive // but when called enough times things can start to add up. if (projectToRetarget.MetadataReferences.Count > 0 && + TryGetReferenceInformation(projectToRetarget.Id, projectUpdateState, out var projectReferenceInfo) && CanConvertMetadataReferenceToProjectReference(solutionChanges.Solution, projectToRetarget, candidateProjectState)) { foreach (var reference in projectToRetarget.MetadataReferences) @@ -578,9 +583,8 @@ private static ProjectUpdateState ConvertMetadataReferencesToProjectReferences_N solutionChanges.UpdateSolutionForProjectAction(projectToRetarget.Id, newSolution); - projectUpdateState = GetReferenceInformation(projectToRetarget.Id, projectUpdateState, out var projectInfo); - projectUpdateState = projectUpdateState.WithProjectReferenceInfo(projectToRetarget.Id, - projectInfo.WithConvertedProjectReference(peReference.FilePath!, projectReference)); + projectReferenceInfo = projectReferenceInfo.WithConvertedProjectReference(peReference.FilePath!, projectReference); + projectUpdateState = projectUpdateState.WithProjectReferenceInfo(projectToRetarget.Id, projectReferenceInfo); // We have converted one, but you could have more than one reference with different aliases that // we need to convert, so we'll keep going @@ -651,7 +655,8 @@ private static ProjectUpdateState ConvertProjectReferencesToMetadataReferences_N { foreach (var projectIdToRetarget in solutionChanges.Solution.ProjectIds) { - projectUpdateState = GetReferenceInformation(projectIdToRetarget, projectUpdateState, out var referenceInfo); + if (!TryGetReferenceInformation(projectIdToRetarget, projectUpdateState, out var referenceInfo)) + continue; // Update ConvertedProjectReferences in place to avoid duplicate list allocations for (var i = 0; i < referenceInfo.ConvertedProjectReferences.Length; i++) @@ -715,7 +720,7 @@ public static ProjectUpdateState TryCreateConvertedProjectReference_NoLock( aliases: properties.Aliases, embedInteropTypes: properties.EmbedInteropTypes); - projectUpdateState = GetReferenceInformation(referencingProjectState.Id, projectUpdateState, out var projectReferenceInfo); + var projectReferenceInfo = GetRequiredReferenceInformation(referencingProjectState.Id, projectUpdateState); projectUpdateState = projectUpdateState.WithProjectReferenceInfo(referencingProjectState.Id, projectReferenceInfo.WithConvertedProjectReference(path, projectReference)); return projectUpdateState; } @@ -742,7 +747,7 @@ public static ProjectUpdateState TryRemoveConvertedProjectReference_NoLock( ProjectUpdateState projectUpdateState, out ProjectReference? projectReference) { - projectUpdateState = GetReferenceInformation(referencingProject, projectUpdateState, out var projectReferenceInformation); + var projectReferenceInformation = GetRequiredReferenceInformation(referencingProject, projectUpdateState); foreach (var convertedProject in projectReferenceInformation.ConvertedProjectReferences) { if (convertedProject.path == path && @@ -770,7 +775,7 @@ public static ProjectUpdateState RemoveProjectOutputPath_NoLock( bool solutionClosing, SolutionServices solutionServices) { - projectUpdateState = GetReferenceInformation(projectId, projectUpdateState, out var projectReferenceInformation); + var projectReferenceInformation = GetRequiredReferenceInformation(projectId, projectUpdateState); if (!projectReferenceInformation.OutputPaths.Contains(outputPath)) { throw new ArgumentException($"Project does not contain output path '{outputPath}'", nameof(outputPath)); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs index b71d70bace63..144018fcb04b 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs @@ -446,6 +446,7 @@ static bool FilterMatches(DeclaredSymbolInfo info, SymbolFilter filter) case DeclaredSymbolInfoKind.Record: case DeclaredSymbolInfoKind.RecordStruct: case DeclaredSymbolInfoKind.Struct: + case DeclaredSymbolInfoKind.Union: return (filter & SymbolFilter.Type) != 0; case DeclaredSymbolInfoKind.Constant: case DeclaredSymbolInfoKind.Constructor: diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs index c302ead8094f..34c14764e036 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectChanges.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis.Diagnostics; namespace Microsoft.CodeAnalysis; @@ -22,76 +23,22 @@ internal ProjectChanges(Project newProject, Project oldProject) public Project NewProject { get; } public IEnumerable GetAddedProjectReferences() - { - var oldRefs = new HashSet(OldProject.ProjectReferences); - foreach (var reference in NewProject.ProjectReferences) - { - if (!oldRefs.Contains(reference)) - { - yield return reference; - } - } - } + => GetChangedProjectReferences(NewProject, OldProject); public IEnumerable GetRemovedProjectReferences() - { - var newRefs = new HashSet(NewProject.ProjectReferences); - foreach (var reference in OldProject.ProjectReferences) - { - if (!newRefs.Contains(reference)) - { - yield return reference; - } - } - } + => GetChangedProjectReferences(OldProject, NewProject); public IEnumerable GetAddedMetadataReferences() - { - var oldMetadata = new HashSet(OldProject.MetadataReferences); - foreach (var metadata in NewProject.MetadataReferences) - { - if (!oldMetadata.Contains(metadata)) - { - yield return metadata; - } - } - } + => GetChangedItems(NewProject.MetadataReferences, OldProject.MetadataReferences); public IEnumerable GetRemovedMetadataReferences() - { - var newMetadata = new HashSet(NewProject.MetadataReferences); - foreach (var metadata in OldProject.MetadataReferences) - { - if (!newMetadata.Contains(metadata)) - { - yield return metadata; - } - } - } + => GetChangedItems(OldProject.MetadataReferences, NewProject.MetadataReferences); public IEnumerable GetAddedAnalyzerReferences() - { - var oldAnalyzerReferences = new HashSet(OldProject.AnalyzerReferences); - foreach (var analyzerReference in NewProject.AnalyzerReferences) - { - if (!oldAnalyzerReferences.Contains(analyzerReference)) - { - yield return analyzerReference; - } - } - } + => GetChangedItems(NewProject.AnalyzerReferences, OldProject.AnalyzerReferences); public IEnumerable GetRemovedAnalyzerReferences() - { - var newAnalyzerReferences = new HashSet(NewProject.AnalyzerReferences); - foreach (var analyzerReference in OldProject.AnalyzerReferences) - { - if (!newAnalyzerReferences.Contains(analyzerReference)) - { - yield return analyzerReference; - } - } - } + => GetChangedItems(OldProject.AnalyzerReferences, NewProject.AnalyzerReferences); /// /// Get s of added documents in the order they appear in of the . @@ -160,4 +107,22 @@ public IEnumerable GetRemovedAdditionalDocuments() /// public IEnumerable GetRemovedAnalyzerConfigDocuments() => NewProject.State.AnalyzerConfigDocumentStates.GetRemovedStateIds(OldProject.State.AnalyzerConfigDocumentStates); + + private static IEnumerable GetChangedItems(IEnumerable newItems, IEnumerable oldItems) + => newItems == oldItems ? [] : newItems.Except(oldItems); + + private static IEnumerable GetChangedProjectReferences(Project newProject, Project oldProject) + { + // Fast path: if the set of projects in the solution and the underlying project references + // collection are identical, then no project references (within the solution) have changed. + if (newProject.Solution.ProjectIds == oldProject.Solution.ProjectIds && + newProject.State.ProjectReferences == oldProject.State.ProjectReferences) + { + return []; + } + + // Compute the diff based on ProjectReferences, which only includes references to projects + // contained in the solution. + return newProject.ProjectReferences.Except(oldProject.ProjectReferences); + } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index 266e2065f5a4..68a31c27d008 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -62,8 +62,7 @@ internal Solution( ImmutableDictionary fallbackAnalyzerOptions) : this(new SolutionCompilationState( new SolutionState(workspace.Kind, workspace.Services.SolutionServices, solutionAttributes, options, analyzerReferences, fallbackAnalyzerOptions), - workspace.PartialSemanticsEnabled, - workspace.GeneratorDriverCreationCache)) + workspace.PartialSemanticsEnabled)) { } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratorDriverInitializationCache.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratorDriverInitializationCache.cs index ce76d23ba9ad..06263f775679 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratorDriverInitializationCache.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratorDriverInitializationCache.cs @@ -3,8 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; -using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; @@ -17,19 +15,20 @@ internal sealed partial class SolutionCompilationState internal sealed class GeneratorDriverInitializationCache { /// - /// A set of GeneratorDriver instances that have been created for the keyed project in the solution. Any time we create a GeneratorDriver the first time for - /// a project, we'll put it into this map. If other requests come in to get a GeneratorDriver for the same project (but from different Solution snapshots), + /// A GeneratorDriver instance that has been created for a project in the solution. Any time we create a GeneratorDriver the first time for + /// a project, we'll hold it in this field. If other requests come in to get a GeneratorDriver for the same project (but from different Solution snapshots), /// well reuse this GeneratorDriver rather than creating a new one. This allows some first time initialization of a generator (like walking metadata references) /// to be shared rather than doing that initialization multiple times. In the case we are reusing a GeneratorDriver, we'll still always update the GeneratorDriver with - /// the current state of the project, so the results are still correct. - /// - /// Since these entries are going to be holding onto non-trivial amounts of state, we get rid of the cached entries once there's a belief that we won't be - /// creating further GeneratorDrivers for a given project. See uses of - /// for details. - /// - /// Any additions/removals to this map must be done via ImmutableInterlocked methods. + /// the current state of the project with a call to , so the results are still correct. + /// + /// This object is held by , and is created when a new project is created, to be shared across + /// all snapshots. + /// + /// When a generator run has happened for a Project, this is assigned an updated AsyncLazy holding the most recent GeneratorDriver from the run. + /// The idea here is if a different fork of the Solution still needs a generator, we have a more recent one. It also ensures that generally the GeneratorDriver + /// being held is "recent", so that way we're not holding onto generator state from a much older run of the solution. /// - private ImmutableDictionary> _driverCache = ImmutableDictionary>.Empty; + private AsyncLazy? _driverCache; public async Task CreateAndRunGeneratorDriverAsync( ProjectState projectState, @@ -37,29 +36,32 @@ public async Task CreateAndRunGeneratorDriverAsync( Func generatorFilter, CancellationToken cancellationToken) { + // If we already have a cached entry setup, just use it; no reason to avoid creating an AsyncLazy we won't use if we can avoid it + var existingDriverCache = _driverCache; + if (existingDriverCache is not null) + { + return UpdateDriverAndRunGenerators(await existingDriverCache.GetValueAsync(cancellationToken).ConfigureAwait(false)); + } + // The AsyncLazy we create here implicitly creates a GeneratorDriver that will run generators for the compilation passed to this method. - // If the one that is added to _driverCache is the one we created, then it's ready to go. If the AsyncLazy is one created by some + // If the one that is set to _driverCache is the one we created, then it's ready to go. If the AsyncLazy is one created by some // other call, then we'll still need to run generators for the compilation passed. var createdAsyncLazy = AsyncLazy.Create(CreateGeneratorDriverAndRunGenerators); - var asyncLazy = ImmutableInterlocked.GetOrAdd(ref _driverCache, projectState.Id, static (_, created) => created, createdAsyncLazy); + var asyncLazy = Interlocked.CompareExchange(ref _driverCache, createdAsyncLazy, comparand: null); - if (asyncLazy == createdAsyncLazy) + if (asyncLazy == null) { // We want to ensure that the driver is always created and initialized at least once, so we'll ensure that runs even if we cancel the request here. // Otherwise the concern is we might keep starting and cancelling the work which is just wasteful to keep doing it over and over again. We do this // in a Task.Run() so if the underlying computation were to run on our thread, we're not blocking our caller from observing cancellation // if they request it. - _ = Task.Run(() => asyncLazy.GetValueAsync(CancellationToken.None)); + _ = Task.Run(() => createdAsyncLazy.GetValueAsync(CancellationToken.None)); - return await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false); + return await createdAsyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false); } else { - var driver = await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false); - - driver = UpdateGeneratorDriverToMatchState(driver, projectState); - - return driver.RunGenerators(compilation, generatorFilter, cancellationToken); + return UpdateDriverAndRunGenerators(await asyncLazy.GetValueAsync(cancellationToken).ConfigureAwait(false)); } GeneratorDriver CreateGeneratorDriverAndRunGenerators(CancellationToken cancellationToken) @@ -77,31 +79,18 @@ GeneratorDriver CreateGeneratorDriverAndRunGenerators(CancellationToken cancella return generatorDriver.RunGenerators(compilation, generatorFilter, cancellationToken); } - } - - public void EmptyCacheForProjectsThatHaveGeneratorDriversInSolution(SolutionCompilationState state) - { - // If we don't have any cached drivers, then just return before we loop through all the projects - // in the solution. This is to ensure that once we hit a steady-state case of a Workspace's CurrentSolution - // having generators for all projects, we won't need to keep anything further in our cache since the cache - // will never be used -- any running of generators in the future will use the GeneratorDrivers already held by - // the Solutions. - // - // This doesn't need to be synchronized against other mutations to _driverCache. If we see it as empty when - // in reality something was just being added, we'll just do the cleanup the next time this method is called. - if (_driverCache.IsEmpty) - return; - foreach (var (projectId, tracker) in state._projectIdToTrackerMap) + GeneratorDriver UpdateDriverAndRunGenerators(GeneratorDriver driver) { - if (tracker.GeneratorDriver is not null) - EmptyCacheForProject(projectId); + driver = UpdateGeneratorDriverToMatchState(driver, projectState); + + return driver.RunGenerators(compilation, generatorFilter, cancellationToken); } } - public void EmptyCacheForProject(ProjectId projectId) + public void UpdateCacheWithGeneratorDriver(GeneratorDriver driver) { - ImmutableInterlocked.TryRemove(ref _driverCache, projectId, out _); + _driverCache = AsyncLazy.Create(driver); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker.cs index 9f1322813104..cf50bc333ed1 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker.cs @@ -525,6 +525,15 @@ await compilationState.GetCompilationAsync( staleCompilationWithGeneratedDocuments.ScriptCompilationInfo!.WithPreviousScriptCompilation(previousSubmissionCompilation!)); } } + else if (!referencedProject.SupportsCompilation) + { + if (referencedProject.OutputFilePath is { } outputPath) + { + var metadataService = ProjectState.LanguageServices.SolutionServices.GetRequiredService(); + var metadataReference = metadataService.GetReference(outputPath, new MetadataReferenceProperties(MetadataImageKind.Assembly, projectReference.Aliases, projectReference.EmbedInteropTypes)); + AddMetadataReference(projectReference, metadataReference); + } + } else { // Not a submission. Add as a metadata reference. diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker_Generators.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker_Generators.cs index 55829e99ea81..04b3b76e815a 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker_Generators.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RegularCompilationTracker_Generators.cs @@ -303,15 +303,24 @@ await newGeneratedDocuments.States.Values.SelectAsArrayAsync( // Hold onto the prior results so we can compare when filtering var priorRunResult = generatorDriver?.GetRunResult(); + var generatorDriverCache = compilationState.GetGeneratorDriverInitializationCache(this.ProjectState.Id); + if (generatorDriver == null) { - generatorDriver = await compilationState.GeneratorDriverCache.CreateAndRunGeneratorDriverAsync(this.ProjectState, compilationToRunGeneratorsOn, ShouldGeneratorRun, cancellationToken).ConfigureAwait(false); + generatorDriver = await generatorDriverCache.CreateAndRunGeneratorDriverAsync(this.ProjectState, compilationToRunGeneratorsOn, ShouldGeneratorRun, cancellationToken).ConfigureAwait(false); } else { generatorDriver = generatorDriver.RunGenerators(compilationToRunGeneratorsOn, ShouldGeneratorRun, cancellationToken); } + // Since this is our most recent run, we'll update our cache with this one. This has two benefits: + // + // 1. If some other fork of this Solution needs a GeneratorDriver created, it'll have one that's probably more update to date. + // This is obviously speculative -- if it's a really old Solution fork it might not help, but can't hurt for the more common cases. + // 2. It ensures that we're not holding an old GeneratorDriver alive, which itself may hold onto state that's no longer applicable. + generatorDriverCache.UpdateCacheWithGeneratorDriver(generatorDriver); + CheckGeneratorDriver(generatorDriver, this.ProjectState); var runResult = generatorDriver.GetRunResult(); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs index a52535e6897e..fb8aa83adfa7 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs @@ -43,11 +43,11 @@ internal sealed partial class SolutionCompilationState public bool PartialSemanticsEnabled { get; } public TextDocumentStates FrozenSourceGeneratedDocumentStates { get; } - public GeneratorDriverInitializationCache GeneratorDriverCache { get; } - // Values for all these are created on demand. private ImmutableSegmentedDictionary _projectIdToTrackerMap; + private readonly ImmutableSegmentedDictionary _generatorDriverInitializationCaches; + /// /// Cache we use to map between unrooted symbols (i.e. assembly, module and dynamic symbols) and the project /// they came from. That way if we are asked about many symbols from the same assembly/module we can answer the @@ -63,16 +63,16 @@ private SolutionCompilationState( bool partialSemanticsEnabled, ImmutableSegmentedDictionary projectIdToTrackerMap, SourceGeneratorExecutionVersionMap sourceGeneratorExecutionVersionMap, + ImmutableSegmentedDictionary generatorDriverInitializationCaches, TextDocumentStates frozenSourceGeneratedDocumentStates, - GeneratorDriverInitializationCache generatorDriverCreationCache, AsyncLazy? cachedFrozenSnapshot = null) { SolutionState = solution; PartialSemanticsEnabled = partialSemanticsEnabled; _projectIdToTrackerMap = projectIdToTrackerMap; SourceGeneratorExecutionVersionMap = sourceGeneratorExecutionVersionMap; + _generatorDriverInitializationCaches = generatorDriverInitializationCaches; FrozenSourceGeneratedDocumentStates = frozenSourceGeneratedDocumentStates; - GeneratorDriverCache = generatorDriverCreationCache; // when solution state is changed, we recalculate its checksum _lazyChecksums = AsyncLazy.Create(static async (self, cancellationToken) => @@ -91,14 +91,13 @@ private SolutionCompilationState( public SolutionCompilationState( SolutionState solution, - bool partialSemanticsEnabled, - GeneratorDriverInitializationCache generatorDriverCreationCache) + bool partialSemanticsEnabled) : this( solution, partialSemanticsEnabled, projectIdToTrackerMap: ImmutableSegmentedDictionary.Empty, sourceGeneratorExecutionVersionMap: SourceGeneratorExecutionVersionMap.Empty, - generatorDriverCreationCache: generatorDriverCreationCache, + generatorDriverInitializationCaches: ImmutableSegmentedDictionary.Empty, frozenSourceGeneratedDocumentStates: TextDocumentStates.Empty) { } @@ -122,16 +121,19 @@ private SolutionCompilationState Branch( SolutionState newSolutionState, ImmutableSegmentedDictionary? projectIdToTrackerMap = null, SourceGeneratorExecutionVersionMap? sourceGeneratorExecutionVersionMap = null, + ImmutableSegmentedDictionary? generatorDriverInitializationCaches = null, TextDocumentStates? frozenSourceGeneratedDocumentStates = null, AsyncLazy? cachedFrozenSnapshot = null) { projectIdToTrackerMap ??= _projectIdToTrackerMap; sourceGeneratorExecutionVersionMap ??= SourceGeneratorExecutionVersionMap; + generatorDriverInitializationCaches ??= _generatorDriverInitializationCaches; frozenSourceGeneratedDocumentStates ??= FrozenSourceGeneratedDocumentStates; if (newSolutionState == this.SolutionState && projectIdToTrackerMap == _projectIdToTrackerMap && sourceGeneratorExecutionVersionMap == SourceGeneratorExecutionVersionMap && + generatorDriverInitializationCaches == _generatorDriverInitializationCaches && frozenSourceGeneratedDocumentStates == FrozenSourceGeneratedDocumentStates) { return this; @@ -142,8 +144,8 @@ private SolutionCompilationState Branch( PartialSemanticsEnabled, projectIdToTrackerMap.Value, sourceGeneratorExecutionVersionMap, + generatorDriverInitializationCaches.Value, frozenSourceGeneratedDocumentStates, - GeneratorDriverCache, cachedFrozenSnapshot); } @@ -312,6 +314,11 @@ private ImmutableSegmentedDictionary CreateCompi /// public SourceGeneratorExecutionVersionMap SourceGeneratorExecutionVersionMap { get; } + private GeneratorDriverInitializationCache GetGeneratorDriverInitializationCache(ProjectId id) + { + return _generatorDriverInitializationCaches[id]; + } + /// public SolutionCompilationState AddProjects(ArrayBuilder projectInfos) { @@ -345,10 +352,19 @@ public SolutionCompilationState AddProjects(ArrayBuilder projectInf versionMapBuilder.Add(projectInfo.Id, new()); var sourceGeneratorExecutionVersionMap = new SourceGeneratorExecutionVersionMap(versionMapBuilder.ToImmutable()); + + // Also create new GeneratorDriverInitializationCaches for the new projects + var generatorDriverInitializationCachesBuilder = _generatorDriverInitializationCaches.ToBuilder(); + foreach (var projectInfo in projectInfos) + generatorDriverInitializationCachesBuilder.Add(projectInfo.Id, new()); + + var generatorDriverInitializationCaches = generatorDriverInitializationCachesBuilder.ToImmutable(); + return Branch( newSolutionState, projectIdToTrackerMap: newTrackerMap, - sourceGeneratorExecutionVersionMap: sourceGeneratorExecutionVersionMap); + sourceGeneratorExecutionVersionMap: sourceGeneratorExecutionVersionMap, + generatorDriverInitializationCaches: generatorDriverInitializationCaches); } /// @@ -357,7 +373,7 @@ public SolutionCompilationState RemoveProjects(ArrayBuilder projectId if (projectIds.Count == 0) return this; - // Now go and remove the projects from teh solution-state itself. + // Now go and remove the projects from the solution-state itself. var newSolutionState = this.SolutionState.RemoveProjects(projectIds); var originalDependencyGraph = this.SolutionState.GetProjectDependencyGraph(); @@ -372,7 +388,7 @@ public SolutionCompilationState RemoveProjects(ArrayBuilder projectId // Now for each compilation tracker. // 1. remove the compilation tracker if we're removing the project. - // 2. fork teh compilation tracker if it depended on a removed project. + // 2. fork the compilation tracker if it depended on a removed project. // 3. do nothing for the rest. var newTrackerMap = CreateCompilationTrackerMap( // Can reuse the compilation tracker for a project, unless it is some project that had a dependency on one @@ -391,10 +407,15 @@ public SolutionCompilationState RemoveProjects(ArrayBuilder projectId foreach (var projectId in projectIds) versionMapBuilder.Remove(projectId); + var generatorDriverInitializationCachesBuilder = _generatorDriverInitializationCaches.ToBuilder(); + foreach (var projectId in projectIds) + generatorDriverInitializationCachesBuilder.Remove(projectId); + return this.Branch( newSolutionState, projectIdToTrackerMap: newTrackerMap, - sourceGeneratorExecutionVersionMap: new(versionMapBuilder.ToImmutable())); + sourceGeneratorExecutionVersionMap: new(versionMapBuilder.ToImmutable()), + generatorDriverInitializationCaches: generatorDriverInitializationCachesBuilder.ToImmutable()); } /// @@ -1496,6 +1517,7 @@ public SolutionCompilationState UpdateSpecificSourceGeneratorExecutionVersions( SourceGeneratorExecutionVersionMap sourceGeneratorExecutionVersions) { var versionMapBuilder = SourceGeneratorExecutionVersionMap.Map.ToBuilder(); + var generatorDriverInitializationCaches = _generatorDriverInitializationCaches.ToBuilder(); var newIdToTrackerMapBuilder = _projectIdToTrackerMap.ToBuilder(); var changed = false; @@ -1510,6 +1532,11 @@ public SolutionCompilationState UpdateSpecificSourceGeneratorExecutionVersions( changed = true; versionMapBuilder[projectId] = sourceGeneratorExecutionVersion; + // We want to clear out any previous initialized generator driver, otherwise the later request for new + // generated documents might be a no-op if we just end up reusing the cached driver that already analyzed + // that compilation. + generatorDriverInitializationCaches[projectId] = new(); + // If we do already have a compilation tracker for this project, then let the tracker know that the source // generator version has changed. We do this by telling it that it should now create SG docs and skeleton // references if they're out of date. @@ -1522,11 +1549,6 @@ public SolutionCompilationState UpdateSpecificSourceGeneratorExecutionVersions( if (newTracker != existingTracker) newIdToTrackerMapBuilder[projectId] = newTracker; } - - // Clear out the cache of any previously initialized GeneratorDriver. Otherwise we might reuse a - // driver which will not count as a new "run" in some of our unit tests. We have tests that very explicitly count - // and assert the number of invocations of a generator. - GeneratorDriverCache.EmptyCacheForProject(projectId); } if (!changed) @@ -1535,7 +1557,8 @@ public SolutionCompilationState UpdateSpecificSourceGeneratorExecutionVersions( return this.Branch( this.SolutionState, projectIdToTrackerMap: newIdToTrackerMapBuilder.ToImmutable(), - sourceGeneratorExecutionVersionMap: new(versionMapBuilder.ToImmutable())); + sourceGeneratorExecutionVersionMap: new(versionMapBuilder.ToImmutable()), + generatorDriverInitializationCaches: generatorDriverInitializationCaches.ToImmutable()); } public SolutionCompilationState WithFrozenPartialCompilations(CancellationToken cancellationToken) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index 712d6cf569b6..04b14b9f4511 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -402,9 +402,6 @@ ProjectState CreateProjectState(ProjectInfo projectInfo) CheckNotContainsProject(projectId); var languageServices = Services.GetLanguageServices(language); - if (languageServices == null) - throw new ArgumentException(string.Format(WorkspacesResources.The_language_0_is_not_supported, language)); - if (!FallbackAnalyzerOptions.TryGetValue(language, out var fallbackAnalyzerOptions)) { fallbackAnalyzerOptions = StructuredAnalyzerConfigOptions.Empty; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs index 06a46edd4eff..9bbf163f05d4 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentStates.cs @@ -142,15 +142,13 @@ public ImmutableArray SelectAsArray(Func AddRange(ImmutableArray states) { - using var pooledIds = SharedPools.Default>().GetPooledObject(); - var ids = pooledIds.Object; + using var _ = ArrayBuilder.GetInstance(discardLargeInstances: false, out var ids); - foreach (var state in states) - ids.Add(state.Id); + ids.AddRange(states, static state => state.Id); return new( _ids.AddRange(ids), - States.AddRange(states.Select(state => KeyValuePair.Create(state.Id, state))), + States.AddRange(states.Select(static state => KeyValuePair.Create(state.Id, state))), filePathToDocumentIds: null); } diff --git a/src/Workspaces/Core/Portable/Workspace/Workspace.cs b/src/Workspaces/Core/Portable/Workspace/Workspace.cs index 4c32aa6986c8..36f997f4bb9e 100644 --- a/src/Workspaces/Core/Portable/Workspace/Workspace.cs +++ b/src/Workspaces/Core/Portable/Workspace/Workspace.cs @@ -50,11 +50,6 @@ public abstract partial class Workspace : IDisposable // this lock guards all the mutable fields (do not share lock with derived classes) private readonly NonReentrantLock _stateLock = new(useThisInstanceForSynchronization: true); - /// - /// Cache for initializing generator drivers across different Solution instances from this Workspace. - /// - internal SolutionCompilationState.GeneratorDriverInitializationCache GeneratorDriverCreationCache { get; } = new(); - /// /// Current solution. Must be locked with when writing to it. /// @@ -280,13 +275,6 @@ internal bool SetCurrentSolution( { data.onAfterUpdate?.Invoke(oldSolution, newSolution); - // The GeneratorDriverCreationCache holds onto a primordial GeneratorDriver for a project when we first creat one. That way, if another fork - // of the Solution also needs to run generators, it's able to reuse that primordial driver rather than recreating one from scratch. We want to - // clean up that cache at some point so we're not holding onto unneeded GeneratorDrivers. We'll clean out some cached entries here for projects - // that have a GeneratorDriver held in CurrentSolution. The idea being that once a project has a GeneratorDriver in the CurrentSolution, all future - // requests for generated documents will just use the updated generator that project already has, so there will never be another need to create one. - data.@this.GeneratorDriverCreationCache.EmptyCacheForProjectsThatHaveGeneratorDriversInSolution(newSolution.CompilationState); - // Queue the event but don't execute its handlers on this thread. // Doing so under the serialization lock guarantees the same ordering of the events // as the order of the changes made to the solution. @@ -1096,7 +1084,7 @@ protected internal void OnDocumentRemoved(DocumentId documentId) WorkspaceChangeKind.DocumentRemoved, documentId: documentId, onBeforeUpdate: (oldSolution, _) => { - // Clear out mutable state not associated with teh solution snapshot (for example, which documents are + // Clear out mutable state not associated with the solution snapshot (for example, which documents are // currently open). this.ClearDocumentData(documentId); }); @@ -1435,7 +1423,7 @@ protected internal void OnAnalyzerConfigDocumentRemoved(DocumentId documentId) WorkspaceChangeKind.AnalyzerConfigDocumentRemoved, documentId: documentId, onBeforeUpdate: (oldSolution, _) => { - // Clear out mutable state not associated with teh solution snapshot (for example, which documents are + // Clear out mutable state not associated with the solution snapshot (for example, which documents are // currently open). this.ClearDocumentData(documentId); }); diff --git a/src/Workspaces/Core/Portable/WorkspacesResources.resx b/src/Workspaces/Core/Portable/WorkspacesResources.resx index 1b22399b7d47..21e42ae9886b 100644 --- a/src/Workspaces/Core/Portable/WorkspacesResources.resx +++ b/src/Workspaces/Core/Portable/WorkspacesResources.resx @@ -234,9 +234,6 @@ Cannot open project '{0}' because the file extension '{1}' is not associated with a language. - - Cannot open project '{0}' because the language '{1}' is not supported. - Invalid project file path: '{0}' diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf index 6f3c02b099a6..e2fab10ab53a 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf @@ -387,11 +387,6 @@ Nejde otevřít projekt {0}, protože přípona souboru {1} není přidružená k jazyku. - - Cannot open project '{0}' because the language '{1}' is not supported. - Nejde otevřít projekt {0}, protože jazyk {1} se nepodporuje. - - Invalid project file path: '{0}' Neplatná cesta k souboru projektu: {0} diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf index ae60405c80f8..ee81859d2dae 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf @@ -387,11 +387,6 @@ Projekt "{0}" kann nicht geöffnet werden, da die Dateierweiterung "{1}" keiner Sprache zugeordnet ist. - - Cannot open project '{0}' because the language '{1}' is not supported. - Projekt "{0}" kann nicht geöffnet werden, da die Sprache "{1}" nicht unterstützt wird. - - Invalid project file path: '{0}' Ungültiger Projektdateipfad: "{0}" diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf index aa2dcd8e1def..a7638ba815a9 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf @@ -387,11 +387,6 @@ No se puede abrir el proyecto '{0}' porque la extensión de archivo '{1}' no está asociada a un lenguaje. - - Cannot open project '{0}' because the language '{1}' is not supported. - No se puede abrir el proyecto '{0}' porque el lenguaje '{1}' no es compatible. - - Invalid project file path: '{0}' Ruta de acceso del archivo de proyecto no válida: '{0}' diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf index 40c6817aaf96..41d9504872ea 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf @@ -387,11 +387,6 @@ Impossible d'ouvrir le projet '{0}', car l'extension de fichier '{1}' n'est pas associée à un langage. - - Cannot open project '{0}' because the language '{1}' is not supported. - Impossible d'ouvrir le projet '{0}', car le langage '{1}' n'est pas pris en charge. - - Invalid project file path: '{0}' Chemin d'accès au fichier de projet non valide : '{0}' diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf index ed94312cb085..efe38c10952c 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf @@ -387,11 +387,6 @@ Non è possibile aprire il progetto '{0}' perché l'estensione di file '{1}' non è associata a un linguaggio. - - Cannot open project '{0}' because the language '{1}' is not supported. - Non è possibile aprire il progetto '{0}' perché il linguaggio '{1}' non è supportato. - - Invalid project file path: '{0}' Percorso del file di progetto non valido: '{0}' diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf index ef15dd8acb00..1c2c39dd7afe 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf @@ -387,11 +387,6 @@ ファイルの拡張子 '{1}' が言語に関連付けられていないため、プロジェクト '{0}' を開くことができません。 - - Cannot open project '{0}' because the language '{1}' is not supported. - 言語 '{1}' がサポートされていないため、プロジェクト '{0}' を開くことができません。 - - Invalid project file path: '{0}' 無効なプロジェクト ファイルのパス: '{0}' diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf index 97cf3eb6b878..443f745f4b2b 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf @@ -387,11 +387,6 @@ '{1}' 파일 확장명이 언어에 연결되어 있지 않아 '{0}' 프로젝트를 열 수 없습니다. - - Cannot open project '{0}' because the language '{1}' is not supported. - '{1}' 언어를 지원하지 않아 '{0}' 프로젝트를 열 수 없습니다. - - Invalid project file path: '{0}' 잘못된 프로젝트 파일 경로입니다('{0}'). diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf index 6fc52f63bf76..baa7c5a9a7e1 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf @@ -387,11 +387,6 @@ Nie można otworzyć projektu „{0}”, ponieważ rozszerzenie pliku „{1}” nie jest skojarzone z językiem. - - Cannot open project '{0}' because the language '{1}' is not supported. - Nie można otworzyć projektu „{0}”, ponieważ język „{1}” nie jest obsługiwany. - - Invalid project file path: '{0}' Nieprawidłowa ścieżka pliku projektu: „{0}” diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf index 56953c04630a..5bc3e24d8c72 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf @@ -387,11 +387,6 @@ Não é possível abrir o projeto "{0}" porque a extensão de arquivo "{1}" não está associada a um idioma. - - Cannot open project '{0}' because the language '{1}' is not supported. - Não é possível abrir o projeto "{0}" porque o idioma "{1}" não é suportado. - - Invalid project file path: '{0}' Caminho do arquivo de projeto inválido: "{0}" diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf index f249d27fe757..f449191ef78e 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf @@ -387,11 +387,6 @@ Не удается открыть проект "{0}", так как расширение файла "{1}" не связано с языком. - - Cannot open project '{0}' because the language '{1}' is not supported. - Не удается открыть проект "{0}", так как не поддерживается язык "{1}". - - Invalid project file path: '{0}' Недействительный путь файла проекта: "{0}" diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf index 86f5e4e2ce62..6a6fb5955bb9 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf @@ -387,11 +387,6 @@ Dosya uzantısı '{1}' bir dil ile ilişkili olmadığı için '{0}' projesi açılamıyor. - - Cannot open project '{0}' because the language '{1}' is not supported. - '{1}' dili desteklenmediği için '{0}' projesi açılamıyor. - - Invalid project file path: '{0}' Geçersiz proje dosyası yolu: '{0}' diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf index 09c6694563df..8686aaa71d06 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf @@ -387,11 +387,6 @@ 无法打开项目“{0}”, 因为文件扩展名“{1}”没有与某种语言关联。 - - Cannot open project '{0}' because the language '{1}' is not supported. - 无法打开项目“{0}”,因为语言“{1}”不受支持。 - - Invalid project file path: '{0}' 无效的项目文件路径:“{0}” diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf index e5902fdec673..586d9654c9c6 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf @@ -387,11 +387,6 @@ 無法開啟專案 '{0}',因為副檔名 '{1}' 未與語言相關聯。 - - Cannot open project '{0}' because the language '{1}' is not supported. - 無法開啟專案 '{0}',因為不支援語言 '{1}'。 - - Invalid project file path: '{0}' 專案檔路徑無效: '{0}' diff --git a/src/Workspaces/CoreTest/EditorConfigParsing/NamingStyleParserTests.cs b/src/Workspaces/CoreTest/EditorConfigParsing/NamingStyleParserTests.cs index 0f71a03ec11f..43510330d271 100644 --- a/src/Workspaces/CoreTest/EditorConfigParsing/NamingStyleParserTests.cs +++ b/src/Workspaces/CoreTest/EditorConfigParsing/NamingStyleParserTests.cs @@ -947,7 +947,7 @@ public void TestParseRoslynEditorConfig() csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after - csharp_space_around_declaration_statements = do_not_ignore + csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false diff --git a/src/Workspaces/CoreTest/FindSymbols/NavigateToSearchIndexTests.cs b/src/Workspaces/CoreTest/FindSymbols/NavigateToSearchIndexTests.cs new file mode 100644 index 000000000000..e94fb0363df2 --- /dev/null +++ b/src/Workspaces/CoreTest/FindSymbols/NavigateToSearchIndexTests.cs @@ -0,0 +1,1299 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.PatternMatching; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.FindSymbols; + +public sealed class NavigateToSearchIndexTests +{ + #region Helpers + + private static DeclaredSymbolInfo CreateInfo(string name, string fullyQualifiedContainerName = "") + { + var stringTable = new StringTable(); + return DeclaredSymbolInfo.Create( + stringTable, name, nameSuffix: null, containerDisplayName: null, + fullyQualifiedContainerName, isPartial: false, hasAttributes: false, + DeclaredSymbolInfoKind.Class, Accessibility.Public, + default, ImmutableArray.Empty); + } + + private static NavigateToSearchIndex CreateIndex(params (string name, string container)[] symbols) + { + var infos = symbols.Select(s => CreateInfo(s.name, s.container)).ToImmutableArray(); + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos); + } + + private static PatternMatch? GetNameMatch(string candidate, string pattern) + { + using var matcher = PatternMatcher.CreatePatternMatcher( + pattern, includeMatchedSpans: false, PatternMatcherKind.Standard | PatternMatcherKind.Fuzzy); + return matcher.GetFirstMatch(candidate); + } + + private static bool GetContainerMatch(string container, string containerPattern) + { + using var matches = TemporaryArray.Empty; + using var matcher = PatternMatcher.CreateDotSeparatedContainerMatcher(containerPattern, includeMatchedSpans: false); + return matcher.AddMatches(container, ref matches.AsRef()); + } + + #endregion + + #region HumpCheck — Mixed-case (bigram-based for multi-hump) + + // The hump set is an exact FrozenSet (not a bloom filter), so all false-case + // assertions are guaranteed — no false positives to worry about. + + [Theory] + // ═══ GooBar ═══ + // Character-parts: "Goo", "Bar" → hump initials: G, B + // Stored: individual chars "G", "B"; bigram "GB" + // + // Single-hump patterns: check that the individual hump-initial character is stored. + [InlineData("GooBar", "G", true)] + [InlineData("GooBar", "B", true)] + [InlineData("GooBar", "Goo", true)] // still single hump (initial 'G') + [InlineData("GooBar", "Bar", true)] // still single hump (initial 'B') + // False: characters that are not hump initials of "GooBar". + [InlineData("GooBar", "A", false)] + [InlineData("GooBar", "C", false)] + [InlineData("GooBar", "D", false)] + [InlineData("GooBar", "E", false)] + [InlineData("GooBar", "F", false)] + [InlineData("GooBar", "H", false)] + [InlineData("GooBar", "X", false)] + [InlineData("GooBar", "Z", false)] + [InlineData("GooBar", "Xyz", false)] // single hump 'X' — not a hump initial of "GooBar" + // + // Multi-hump patterns: check adjacent bigrams in the hump set. + // "GB" checks bigram G→B, which is stored for "GooBar". + [InlineData("GooBar", "GB", true)] + [InlineData("GooBar", "GoBa", true)] // same humps G, B + // False: bigrams that don't exist for "GooBar". + [InlineData("GooBar", "GA", false)] // G→A: 'A' not a hump initial + [InlineData("GooBar", "GC", false)] // G→C: 'C' not a hump initial + [InlineData("GooBar", "GX", false)] // G→X: 'X' not a hump initial + [InlineData("GooBar", "GZ", false)] // G→Z: 'Z' not a hump initial + [InlineData("GooBar", "BG", false)] // B→G: wrong order (only G→B is stored) + [InlineData("GooBar", "BA", false)] // B→A: 'A' not a hump initial + [InlineData("GooBar", "BX", false)] // B→X: 'X' not a hump initial + [InlineData("GooBar", "AB", false)] // A→B: 'A' not a hump initial + [InlineData("GooBar", "XY", false)] // neither X nor Y are hump initials + [InlineData("GooBar", "XZ", false)] // neither X nor Z are hump initials + // + // ═══ GooBarQuux ═══ + // Character-parts: "Goo", "Bar", "Quux" → hump initials: G, B, Q + // Stored: "G", "B", "Q"; bigrams "GB", "GQ", "BQ" (all ordered pairs) + // + // Three humps: "GBQ" checks pairs G→B and B→Q. + [InlineData("GooBarQuux", "GBQ", true)] + [InlineData("GooBarQuux", "GoBarQu", true)] + // Non-contiguous: "GQ" checks bigram G→Q, stored because all ordered pairs are indexed. + [InlineData("GooBarQuux", "GQ", true)] + [InlineData("GooBarQuux", "GoQu", true)] + [InlineData("GooBarQuux", "BQ", true)] // B→Q stored + // False: wrong-order bigrams. + [InlineData("GooBarQuux", "QG", false)] // Q→G: wrong order + [InlineData("GooBarQuux", "QB", false)] // Q→B: wrong order + [InlineData("GooBarQuux", "BG", false)] // B→G: wrong order + [InlineData("GooBarQuux", "QBG", false)] // Q→B: wrong order (first pair fails) + // False: one valid pair + one invalid pair. + [InlineData("GooBarQuux", "GBX", false)] // G→B ok, B→X not stored + [InlineData("GooBarQuux", "GXQ", false)] // G→X not stored (first pair fails) + [InlineData("GooBarQuux", "XBQ", false)] // X→B not stored (first pair fails) + // + // ═══ XMLDocument ═══ + // Character-parts: "X", "M", "L", "Document" → hump initials: X, M, L, D + // Stored: "X", "M", "L", "D"; bigrams "XM", "XL", "XD", "ML", "MD", "LD" + // + [InlineData("XMLDocument", "XD", true)] // skip M and L + [InlineData("XMLDocument", "XM", true)] + [InlineData("XMLDocument", "ML", true)] + [InlineData("XMLDocument", "LD", true)] + [InlineData("XMLDocument", "XL", true)] // non-adjacent pair X→L + [InlineData("XMLDocument", "MD", true)] // non-adjacent pair M→D + [InlineData("XMLDocument", "XMLD", true)] // X→M, M→L, L→D (chain of 3 adjacent pairs) + // False: wrong-order bigrams. + [InlineData("XMLDocument", "DX", false)] // D→X: wrong order + [InlineData("XMLDocument", "DM", false)] // D→M: wrong order + [InlineData("XMLDocument", "LM", false)] // L→M: wrong order + [InlineData("XMLDocument", "LX", false)] // L→X: wrong order + [InlineData("XMLDocument", "DL", false)] // D→L: wrong order + // False: absent hump initials. + [InlineData("XMLDocument", "XA", false)] // 'A' not a hump initial + [InlineData("XMLDocument", "XZ", false)] // 'Z' not a hump initial + [InlineData("XMLDocument", "ZD", false)] // 'Z' not a hump initial + [InlineData("XMLDocument", "A", false)] // 'A' not a hump initial + [InlineData("XMLDocument", "Z", false)] // 'Z' not a hump initial + // + // ═══ CodeFixProvider ═══ + // Character-parts: "Code", "Fix", "Provider" → hump initials: C, F, P + // Stored: "C", "F", "P"; bigrams "CF", "CP", "FP" + // + [InlineData("CodeFixProvider", "CF", true)] + [InlineData("CodeFixProvider", "CP", true)] // non-adjacent C→P + [InlineData("CodeFixProvider", "FP", true)] + [InlineData("CodeFixProvider", "CFP", true)] // C→F, F→P + // False: wrong-order bigrams. + [InlineData("CodeFixProvider", "FC", false)] // F→C: wrong order + [InlineData("CodeFixProvider", "PC", false)] // P→C: wrong order + [InlineData("CodeFixProvider", "PF", false)] // P→F: wrong order + // False: absent hump initials. + [InlineData("CodeFixProvider", "CA", false)] // 'A' not a hump initial + [InlineData("CodeFixProvider", "CX", false)] // 'X' not a hump initial + [InlineData("CodeFixProvider", "A", false)] // 'A' not a hump initial + [InlineData("CodeFixProvider", "X", false)] // 'X' not a hump initial + public void HumpCheck_MixedCase(string symbolName, string pattern, bool expected) + { + var index = CreateIndex((symbolName, "")); + Assert.Equal(expected, index.GetTestAccessor().HumpCheckPasses(pattern)); + } + + #endregion + + #region HumpCheck — Cross-symbol bigram rejection + + /// + /// Verifies that bigram-based hump checks are more selective than individual character checks. + /// When two symbols contribute different hump initials (e.g., "Goo" has 'G' and "Xyz" has 'X'), + /// the bigram "GX" is NOT stored because no single symbol has humps G→X. This eliminates + /// cross-symbol false positives that the old individual-character approach would have missed. + /// Since the hump data is stored in an exact FrozenSet<T>, these rejections are + /// guaranteed. + /// + [Fact] + public void HumpCheck_CrossSymbolBigram_Rejected() + { + var index = CreateIndex(("Goo", ""), ("Xyz", "")); + + // Individual chars: both 'G' and 'X' are in the set. + Assert.True(index.GetTestAccessor().HumpCheckPasses("G")); + Assert.True(index.GetTestAccessor().HumpCheckPasses("X")); + + // Bigram: "GX" is NOT stored because no single symbol has hump pair G→X. + Assert.False(index.GetTestAccessor().HumpCheckPasses("GX")); + + // Also verify the reverse. + Assert.False(index.GetTestAccessor().HumpCheckPasses("XG")); + } + + #endregion + + #region HumpCheck — All-lowercase (hump prefix DP) + + [Theory] + // All-lowercase patterns use a DP that splits the pattern into segments, each of which must + // be a lowercased prefix of some hump in the document. + // + // ═══ GooBar ═══ + // Humps: "Goo", "Bar" + // Stored hump prefixes: "g", "go", "goo", "b", "ba", "bar" + // + // True: single-hump prefix matches. + [InlineData("GooBar", "g", true)] // "g" → prefix of "goo" + [InlineData("GooBar", "go", true)] // "go" → prefix of "goo" + [InlineData("GooBar", "goo", true)] // "goo" → full first hump + [InlineData("GooBar", "b", true)] // "b" → prefix of "bar" + [InlineData("GooBar", "ba", true)] // "ba" → prefix of "bar" + [InlineData("GooBar", "bar", true)] // "bar" → full second hump + // + // True: multi-segment DP splits. + [InlineData("GooBar", "gb", true)] // "g"+"b" + [InlineData("GooBar", "goba", true)] // "go"+"ba" + [InlineData("GooBar", "goobar", true)] // "goo"+"bar" (full name lowercased) + [InlineData("GooBar", "goob", true)] // "goo"+"b" + [InlineData("GooBar", "gbar", true)] // "g"+"bar" + [InlineData("GooBar", "barg", true)] // "bar"+"g" (DP doesn't enforce hump order) + // + // False: first character not a hump prefix. + [InlineData("GooBar", "x", false)] // 'x' not a prefix of "goo" or "bar" + [InlineData("GooBar", "xyz", false)] // 'x' not a prefix of any hump + // + // False: valid prefix followed by invalid character. + [InlineData("GooBar", "gx", false)] // "g" ok, "x" not a hump prefix + [InlineData("GooBar", "goox", false)] // "goo" ok, "x" not a hump prefix + [InlineData("GooBar", "barx", false)] // "bar" ok, "x" not a hump prefix + [InlineData("GooBar", "gooxyz", false)] // "goo" ok, "x" not a hump prefix → break + // + // False: middle segment is invalid (DP can't bridge the gap). + [InlineData("GooBar", "gxb", false)] // "g" ok, "x" fails → can't reach "b" + [InlineData("GooBar", "gxba", false)] // "g" ok, "x" fails → can't reach "ba" + // + // False: valid substring exists but can't align with hump boundaries. + // (Note: single-char like "o" can hit hump-prefix bloom filter false positives with small + // datasets, so we use 'x' which is known not to collide with "GooBar" hump prefixes.) + [InlineData("GooBar", "ar", false)] // "ar" is within "Bar" but not a hump prefix + // + // False: pattern too long to split into available humps. + [InlineData("GooBar", "goobarx", false)] // full name matches but trailing "x" fails + [InlineData("GooBar", "goobarbaz", false)] // "goo"+"bar" ok, "baz" fails + [InlineData("GooBar", "gobaquuxbaz", false)] // "go"+"ba" ok, "quuxbaz" has no valid split + [InlineData("GooBar", "xyzxyzxyzxyzxyz", false)] // 'x' not a hump prefix at all + // + // ═══ GooBarQuux ═══ + // Humps: "Goo", "Bar", "Quux" + // Stored hump prefixes: "g","go","goo", "b","ba","bar", "q","qu","quu","quux" + // + // True: multi-hump splits. + [InlineData("GooBarQuux", "gbq", true)] // "g"+"b"+"q" + [InlineData("GooBarQuux", "gobarqu", true)] // "go"+"bar"+"qu" + [InlineData("GooBarQuux", "goobarquux", true)] // full name lowercased + [InlineData("GooBarQuux", "gq", true)] // "g"+"q" (skip "Bar" — non-contiguous) + [InlineData("GooBarQuux", "goqu", true)] // "go"+"qu" (skip "Bar") + [InlineData("GooBarQuux", "bq", true)] // "b"+"q" (start at second hump) + [InlineData("GooBarQuux", "barquux", true)] // "bar"+"quux" + // + // False: invalid segments with three humps. + [InlineData("GooBarQuux", "gbx", false)] // "g"+"b" ok, "x" not a hump prefix + [InlineData("GooBarQuux", "gxq", false)] // "g" ok, "x" fails → can't reach "q" + [InlineData("GooBarQuux", "goobarzuux", false)] // "goo"+"bar" ok, "zuux" fails ('z' not a prefix) + // + // ═══ CodeFixProvider ═══ + // Humps: "Code", "Fix", "Provider" + // Stored hump prefixes: "c","co","cod","code", "f","fi","fix", "p","pr","pro","prov",...,"provider" + // + // True: realistic CamelCase searches. + [InlineData("CodeFixProvider", "cfp", true)] // "c"+"f"+"p" + [InlineData("CodeFixProvider", "cofi", true)] // "co"+"fi" + [InlineData("CodeFixProvider", "cofixpro", true)] // "co"+"fix"+"pro" + [InlineData("CodeFixProvider", "codefixprovider", true)] // full name lowercased + // + // False: segments that aren't hump prefixes. + [InlineData("CodeFixProvider", "cofyz", false)] // "co" ok, "f" ok, "yz" fails + [InlineData("CodeFixProvider", "codxfix", false)] // "cod" ok, "x" fails → can't reach "fix" + [InlineData("CodeFixProvider", "cp", true)] // "c"+"p" → skip "Fix" (non-contiguous) — TRUE + [InlineData("CodeFixProvider", "cx", false)] // "c" ok, "x" not a hump prefix + // + // ═══ XMLDocument ═══ + // Humps: "X", "M", "L", "Document" (4 single-letter humps + one word) + // Stored hump prefixes: "x", "m", "l", "d","do","doc","docu","docum","docume","documen","document" + // + [InlineData("XMLDocument", "xmld", true)] // "x"+"m"+"l"+"d" + [InlineData("XMLDocument", "xdoc", true)] // "x"+"doc" (skip M, L) + [InlineData("XMLDocument", "xmldocument", true)] // full name lowercased + // (Note: certain single chars like 'z' can hit hump-prefix bloom filter false positives + // with small datasets, so we use multi-char invalid segments for more robust rejections.) + [InlineData("XMLDocument", "xmyw", false)] // "x"+"m" ok, "yw" not a hump prefix + [InlineData("XMLDocument", "xmlyw", false)] // "x"+"m"+"l" ok, "yw" not a hump prefix + public void HumpCheck_AllLowercase(string symbolName, string pattern, bool expected) + { + var index = CreateIndex((symbolName, "")); + Assert.Equal(expected, index.GetTestAccessor().HumpCheckPasses(pattern)); + } + + /// + /// Demonstrates that the hump prefix DP correctly allows cross-symbol matches (both + /// hump prefixes are from the document, even if from different symbols) and rejects + /// patterns that contain segments not present in any symbol's hump prefixes. + /// + [Fact] + public void HumpCheck_AllLowercase_CrossSymbolBehavior() + { + var index = CreateIndex(("Goo", ""), ("Xab", "")); + + // Single-hump prefixes from each symbol work. + Assert.True(index.GetTestAccessor().HumpCheckPasses("goo")); + Assert.True(index.GetTestAccessor().HumpCheckPasses("xab")); + + // Cross-symbol: "gx" splits as "g"+"x" — "g" is a prefix of "Goo" and "x" is a + // prefix of "Xab". These are from different symbols, but the hump prefix filter + // stores all hump prefixes from the whole document, so the DP passes. This is + // inherent to document-level filtering. + Assert.True(index.GetTestAccessor().HumpCheckPasses("gx")); + + // "gooxab" splits as "goo"+"xab" — both stored, so passes (cross-symbol). + Assert.True(index.GetTestAccessor().HumpCheckPasses("gooxab")); + } + + /// + /// Verifies the "bar" matches "GooBar" via hump prefix (the second hump), not just + /// the first character. + /// + [Fact] + public void HumpCheck_AllLowercase_SecondHumpPrefix() + { + var index = CreateIndex(("GooBar", "")); + + // "bar" as a full second hump prefix + Assert.True(index.GetTestAccessor().HumpCheckPasses("bar")); + + // "ba" as a partial second hump prefix + Assert.True(index.GetTestAccessor().HumpCheckPasses("ba")); + + // "bax" — "ba" ok but "x" is not a hump prefix + Assert.False(index.GetTestAccessor().HumpCheckPasses("bax")); + } + + /// + /// Tests the break optimization in AllLowercaseHumpCheckPasses. The DP breaks + /// immediately when extending a segment from position i fails, because stored + /// hump prefixes are prefix-closed: if "go" is a hump prefix then "g" must also be one, + /// and if "gob" is NOT a prefix then "goba", "gobar" etc. also can't be. The break makes + /// the inner loop O(max_hump_length) instead of O(n), but must not cause false negatives. + /// + [Fact] + public void HumpCheck_AllLowercase_BreakOptimization_NonGreedySplitSucceeds() + { + // Symbol "GooBar" — humps "Goo", "Bar" + // Stored hump prefixes: "g", "go", "goo", "b", "ba", "bar" + var index = CreateIndex(("GooBar", "")); + + // Pattern "gobar": a greedy approach from position 0 would consume "go" then try + // "gob" which fails → break. But "go" already marked position 2 as reachable, and + // from position 2, "bar" succeeds. Split: "go"+"bar". + Assert.True(index.GetTestAccessor().HumpCheckPasses("gobar")); + + // Pattern "gooba": from position 0, "g"→"go"→"goo" succeed, "goob" fails → break. + // Position 3 is reachable (via "goo"). From position 3, "b"→"ba" succeed. + // canReach[5] = true. Split: "goo"+"ba". + Assert.True(index.GetTestAccessor().HumpCheckPasses("gooba")); + + // Pattern "goobarg": from position 0, "goo" marks 3. From 3, "bar" marks 6. + // From 6, "g" marks 7. Split: "goo"+"bar"+"g". + Assert.True(index.GetTestAccessor().HumpCheckPasses("goobarg")); + + // Pattern "gobarg": from 0, "g" marks 1, "go" marks 2, "gob" fails → break. + // From 1, "o" fails → break. From 2, "b" marks 3, "ba" marks 4, "bar" marks 5, + // "barg" fails → break. From 3, "a" fails → break. From 4, "r" fails → break. + // From 5, "g" marks 6. canReach[6] = true. Split: "go"+"bar"+"g". + Assert.True(index.GetTestAccessor().HumpCheckPasses("gobarg")); + } + + [Fact] + public void HumpCheck_AllLowercase_BreakOptimization_BreakPreventsUnreachablePositions() + { + // Symbol "GooBar" — humps "Goo", "Bar" + // Stored hump prefixes: "g", "go", "goo", "b", "ba", "bar" + var index = CreateIndex(("GooBar", "")); + + // Pattern "gxbar": from 0, "g" marks 1, "gx" fails → break (no "gxb", "gxba", etc.). + // From 1, "x" fails → break. Positions 2-5 never become reachable. + // Even though "bar" exists in the filter, position 2 is never reached, so "bar" + // starting at position 2 is never tried. Result: false. + Assert.False(index.GetTestAccessor().HumpCheckPasses("gxbar")); + + // Pattern "gooxbar": from 0, "goo" marks 3, "goox" fails → break. + // From 1-2: "o"/"o" fail. From 3, "x" fails → break. + // Positions 4-7 never reachable. Result: false. + Assert.False(index.GetTestAccessor().HumpCheckPasses("gooxbar")); + + // Pattern "gxb": from 0, "g" marks 1, "gx" fails → break. + // From 1, "x" fails → break. Position 2 never reachable, "b" never tried. False. + Assert.False(index.GetTestAccessor().HumpCheckPasses("gxb")); + } + + [Fact] + public void HumpCheck_AllLowercase_BreakOptimization_MultipleReachablePositions() + { + // Symbol "GooBarBaz" — humps "Goo", "Bar", "Baz" + // Stored hump prefixes: "g","go","goo", "b","ba","bar","baz" + // (Note: "ba" is a prefix of both "Bar" and "Baz", stored once) + var index = CreateIndex(("GooBarBaz", "")); + + // Pattern "gbba": from 0, "g" marks 1. From 1, "b" marks 2, "bb" fails → break. + // From 2, "b" marks 3, "ba" marks 4. canReach[4] = true. Split: "g"+"b"+"ba". + Assert.True(index.GetTestAccessor().HumpCheckPasses("gbba")); + + // Pattern "gbbar": from 0, "g" marks 1. From 1, "b" marks 2, "bb" fails → break. + // From 2, "b" marks 3, "ba" marks 4, "bar" marks 5. Split: "g"+"b"+"bar". + Assert.True(index.GetTestAccessor().HumpCheckPasses("gbbar")); + + // Pattern "goobarbaz": full name lowercased. Split: "goo"+"bar"+"baz". + Assert.True(index.GetTestAccessor().HumpCheckPasses("goobarbaz")); + + // Pattern "goobazbaz": "goo" marks 3, from 3 "baz" marks 6, from 6 "baz" marks 9. + // Split: "goo"+"baz"+"baz" (DP allows reusing hump prefixes). + Assert.True(index.GetTestAccessor().HumpCheckPasses("goobazbaz")); + + // Pattern "goobxbaz": "goo" marks 3, from 3 "b" marks 4, "bx" fails → break. + // From 4, "x" fails → break. Position 4 reachable but "xbaz" can't split. + // canReach[8] = false. + Assert.False(index.GetTestAccessor().HumpCheckPasses("goobxbaz")); + } + + #endregion + + #region TrigramCheck + + [Theory] + // ═══ Readline ═══ + // Word-part: "Readline" (one word-part since 'R' followed by lowercase). + // Stored trigrams: "rea", "ead", "adl", "dli", "lin", "ine". + // + // True: all trigrams of the pattern are present. + [InlineData("Readline", "rea", true)] + [InlineData("Readline", "ead", true)] + [InlineData("Readline", "adl", true)] + [InlineData("Readline", "dli", true)] + [InlineData("Readline", "lin", true)] + [InlineData("Readline", "ine", true)] + [InlineData("Readline", "read", true)] // trigrams: "rea", "ead" — both stored + [InlineData("Readline", "line", true)] // trigrams: "lin", "ine" — both stored + [InlineData("Readline", "readline", true)] // all trigrams present + [InlineData("Readline", "adli", true)] // trigrams: "adl", "dli" — both stored + // + // False: pattern contains a trigram not present in the filter. + [InlineData("Readline", "xyz", false)] // "xyz" not stored + [InlineData("Readline", "rex", false)] // "rex" not stored + [InlineData("Readline", "reax", false)] // "rea" stored, but "eax" not stored + [InlineData("Readline", "xead", false)] // "xea" not stored (even though "ead" is) + [InlineData("Readline", "readz", false)] // "rea","ead" ok, but "adz" not stored + [InlineData("Readline", "linex", false)] // "lin","ine" ok, but "nex" not stored + [InlineData("Readline", "readlinex", false)] // all original trigrams ok, but trailing "nex" fails + // + // False: too short for trigrams (need at least 3 characters). + [InlineData("Readline", "re", false)] + [InlineData("Readline", "r", false)] + // + // False: mixed-case patterns are not checked by the trigram filter. + [InlineData("Readline", "Read", false)] + [InlineData("Readline", "Line", false)] + [InlineData("Readline", "REA", false)] + // + // ═══ Combine ═══ + // Word-part: "Combine" + // Stored trigrams: "com", "omb", "mbi", "bin", "ine". + // + [InlineData("Combine", "com", true)] + [InlineData("Combine", "bin", true)] + [InlineData("Combine", "bine", true)] // "bin", "ine" — both stored + [InlineData("Combine", "comb", true)] // "com", "omb" — both stored + [InlineData("Combine", "combine", true)] // all trigrams present + [InlineData("Combine", "xyz", false)] // no matching trigrams + [InlineData("Combine", "comx", false)] // "com" ok, but "omx" not stored + [InlineData("Combine", "xbin", false)] // "xbi" not stored + [InlineData("Combine", "binez", false)] // "bin","ine" ok, but "nez" not stored + // + // ═══ GooBar ═══ + // Word-parts: "Goo" and "Bar" (two separate word-parts from CamelCase). + // Stored trigrams: "goo" (from "Goo"), "bar" (from "Bar"). + // Each word-part is only 3 chars, so exactly one trigram each. + // + [InlineData("GooBar", "goo", true)] + [InlineData("GooBar", "bar", true)] + // False: cross-word-part trigrams are NOT stored. + [InlineData("GooBar", "oob", false)] // spans "Goo" → "Bar" boundary + [InlineData("GooBar", "oba", false)] // spans boundary + // + // ═══ Longer symbol: "Transformer" ═══ + // Word-part: "Transformer" + // Stored trigrams: "tra","ran","ans","nsf","sfo","for","orm","rme","mer" + // + [InlineData("Transformer", "tra", true)] + [InlineData("Transformer", "transform", true)] // "tra","ran","ans","nsf","sfo","for","orm" — all stored + [InlineData("Transformer", "former", true)] // "for","orm","rme","mer" — all stored + [InlineData("Transformer", "transformer", true)] + [InlineData("Transformer", "xyz", false)] + [InlineData("Transformer", "trax", false)] // "tra" ok, "rax" not stored + [InlineData("Transformer", "formerx", false)] // "for","orm","rme","mer" ok, but "erx" not stored + public void TrigramCheck(string symbolName, string pattern, bool expected) + { + var index = CreateIndex((symbolName, "")); + Assert.Equal(expected, index.GetTestAccessor().TrigramCheckPasses(pattern)); + } + + #endregion + + #region LengthCheck + + [Theory] + // Thresholds mirror WordSimilarityChecker.GetThreshold: + // pattern.Length < 3 → false (fuzzy disabled, per WordSimilarityChecker.MinFuzzyLength) + // pattern.Length 3–5 → threshold ±1 + // pattern.Length >= 6 → threshold ±2 + // + // ═══ "GooBar" has length 6 ═══ + // + // Pattern length 3 (threshold ±1): symbol length 6, delta = -3 → false. + [InlineData("GooBar", "abc", false)] + // Pattern length 4 (threshold ±1): symbol length 6, delta = -2 → false (exceeds ±1). + [InlineData("GooBar", "abcd", false)] + // Pattern length 5 (threshold ±1): symbol length 6, delta = -1 → true (within ±1). + [InlineData("GooBar", "abcde", true)] + // Pattern length 6 (threshold ±2): delta = 0 → true. + [InlineData("GooBar", "abcdef", true)] + // Pattern length 7 (threshold ±2): delta = +1 → true. + [InlineData("GooBar", "abcdefg", true)] + // Pattern length 8 (threshold ±2): delta = +2 → true (boundary). + [InlineData("GooBar", "abcdefgh", true)] + // Pattern length 9 (threshold ±2): delta = +3 → false. + [InlineData("GooBar", "abcdefghi", false)] + // Pattern length 2: below MinFuzzyLength → false. + [InlineData("GooBar", "ab", false)] + // Pattern length 1: below MinFuzzyLength → false. + [InlineData("GooBar", "a", false)] + // Pattern length 16: far outside → false. + [InlineData("GooBar", "abcdefghijklmnop", false)] + // + // ═══ Short symbol: "Xy" has length 2 ═══ + // + // All patterns with length < 3 are rejected. For length 3 (threshold ±1): symbol length 2, + // delta = -1 → true. + [InlineData("Xy", "a", false)] // length 1 < MinFuzzyLength + [InlineData("Xy", "ab", false)] // length 2 < MinFuzzyLength + [InlineData("Xy", "abc", true)] // length 3, threshold ±1, delta = -1 + [InlineData("Xy", "abcd", false)] // length 4, threshold ±1, delta = -2 → exceeds ±1 + [InlineData("Xy", "abcde", false)] // length 5, threshold ±1, delta = -3 → exceeds ±1 + // + // ═══ Short symbol: "Abc" has length 3 ═══ + // + [InlineData("Abc", "abc", true)] // length 3, threshold ±1, delta = 0 + [InlineData("Abc", "abcd", true)] // length 4, threshold ±1, delta = +1 + [InlineData("Abc", "ab", false)] // length 2 < MinFuzzyLength + [InlineData("Abc", "abcde", false)] // length 5, threshold ±1, checks 4..6. Symbol 3 not in range. + [InlineData("Abc", "abcdef", false)] // length 6, threshold ±2, checks 4..8. Symbol 3 not in range. + // + // ═══ Long symbol: "CodeFixProviderService" has length 22 ═══ + // + [InlineData("CodeFixProviderService", "abcdefghijklmnopqrst", true)] // length 20, delta = -2 + [InlineData("CodeFixProviderService", "abcdefghijklmnopqrstuvwx", true)] // length 24, delta = +2 + [InlineData("CodeFixProviderService", "abcdefghijklmnopqrs", false)] // length 19, delta = -3 + [InlineData("CodeFixProviderService", "abcdefghijklmnopqrstuvwxy", false)] // length 25, delta = +3 + public void LengthCheck(string symbolName, string pattern, bool expected) + { + var index = CreateIndex((symbolName, "")); + Assert.Equal(expected, index.GetTestAccessor().LengthCheckPasses(pattern)); + } + + #endregion + + #region BigramCountCheck + + // The fuzzy bigram bitset is an exact bitset (37x37 = 1369 bits, zero false positives for + // a-z and 0-9 characters). The q-gram count lemma (Ukkonen, 1992) states: if + // edit_distance(s, t) <= k, then at least |s|-1-2k of s's bigrams must appear in t. + // + // See: Ukkonen, E. (1992). "Approximate string-matching with q-grams and maximal matches." + // Theoretical Computer Science, 92(1), 191-211. https://doi.org/10.1016/0304-3975(92)90143-4 + + [Theory] + // ═══ Length 3 (k=1, min_shared = 3-1-2 = 0): no filtering possible, always true ═══ + [InlineData("GooBar", "abc", true)] // all 2 bigrams miss, but min_shared=0 → true + [InlineData("GooBar", "xyz", true)] // completely disjoint, but min_shared=0 → true + // + // ═══ Length 4 (k=1, min_shared = 4-1-2 = 1): need ≥ 1 of 3 bigrams ═══ + [InlineData("GooBar", "goob", true)] // bigrams "go","oo","ob" — all 3 match → true + [InlineData("GooBar", "xoob", true)] // bigrams "xo","oo","ob" — 2 match ("oo","ob") ≥ 1 → true + [InlineData("GooBar", "xyzw", false)] // bigrams "xy","yz","zw" — 0 match < 1 → false + [InlineData("GooBar", "goxx", true)] // bigrams "go","ox","xx" — 1 match ("go") ≥ 1 → true + // + // ═══ Length 5 (k=1, min_shared = 5-1-2 = 2): need ≥ 2 of 4 bigrams ═══ + [InlineData("GooBar", "gooba", true)] // bigrams "go","oo","ob","ba" — all 4 match ≥ 2 → true + [InlineData("GooBar", "xyzwv", false)] // bigrams "xy","yz","zw","wv" — 0 match < 2 → false + [InlineData("GooBar", "goxyz", false)] // bigrams "go","ox","xy","yz" — 1 match ("go") < 2 → false + [InlineData("GooBar", "gooxy", true)] // bigrams "go","oo","ox","xy" — 2 match ("go","oo") ≥ 2 → true + // + // ═══ Length 6 (k=2, min_shared = 6-1-4 = 1): need ≥ 1 of 5 bigrams ═══ + [InlineData("GooBar", "goobar", true)] // all 5 match → true + [InlineData("GooBar", "xyzwvq", false)] // 0 of 5 match < 1 → false + [InlineData("GooBar", "xooxxx", true)] // "xo","oo","ox","xx","xx" — 1 match ("oo") ≥ 1 → true + // + // ═══ Length 7 (k=2, min_shared = 7-1-4 = 2): need ≥ 2 of 6 bigrams ═══ + [InlineData("GooBar", "goobxyz", true)] // "go","oo","ob","bx","xy","yz" — 3 match ≥ 2 → true + [InlineData("GooBar", "xyzwvqu", false)] // 0 of 6 match < 2 → false + [InlineData("GooBar", "xooxyzq", false)] // "xo","oo","ox","xy","yz","zq" — 1 match ("oo") < 2 → false + // + // ═══ Length 8 (k=2, min_shared = 8-1-4 = 3): need ≥ 3 of 7 bigrams ═══ + [InlineData("GooBar", "goobarzz", true)] // "go","oo","ob","ba","ar","rz","zz" — 5 match ≥ 3 → true + [InlineData("GooBar", "xyzwvqup", false)] // 0 of 7 match < 3 → false + [InlineData("GooBar", "gooxxxxx", false)] // "go","oo","ox","xx","xx","xx","xx" — 2 match ("go","oo") < 3 → false + // + // ═══ Length 10 (k=2, min_shared = 10-1-4 = 5): need ≥ 5 of 9 bigrams ═══ + // This demonstrates strong filtering for longer patterns. + [InlineData("GooBar", "goobarxyzw", true)] // "go","oo","ob","ba","ar","rx","xy","yz","zw" — 5 match ≥ 5 → true + [InlineData("GooBar", "xyzwvquprt", false)] // 0 of 9 < 5 → false + // + // ═══ Interesting case: single edit distance ═══ + // "GooBar" vs "XooBar" (1 edit). Bigrams of "xoobar": "xo","oo","ob","ba","ar". + // Of the 5 bigrams, "oo","ob","ba","ar" match (4), only "xo" doesn't. For k=2, + // min_shared=1, so 4 ≥ 1 → passes correctly. + [InlineData("GooBar", "XooBar", true)] + // "GooBar" vs "GooXar" (1 edit). Bigrams of "gooxar": "go","oo","ox","xa","ar". + // Matches: "go","oo","ar" = 3 ≥ 1 → passes correctly. + [InlineData("GooBar", "GooXar", true)] + // + // ═══ Underscore / digits / Unicode ═══ + // Underscore gets its own index (36); characters outside a-z, 0-9, _ map to "other" (37). + [InlineData("Goo_Bar", "go_b", true)] // "_" has index 36; bigrams "o_" and "_b" stored → matches + [InlineData("Goo_Bar", "o_ba", true)] // bigrams "o_","_b","ba" all stored + [InlineData("Test123", "st12", true)] // digits get unique indices; "st","t1","12" all stored + [InlineData("Test123", "st99", true)] // "st" matches (≥1), "t9","99" don't, but 1 ≥ 1 → true + public void BigramCountCheck(string symbolName, string pattern, bool expected) + { + var index = CreateIndex((symbolName, "")); + Assert.Equal(expected, index.GetTestAccessor().BigramCountCheckPasses(pattern)); + } + + /// + /// Verifies that bigrams are accumulated across multiple symbols in the same document. + /// A bigram present in any symbol passes the check. + /// + [Fact] + public void BigramCountCheck_MultipleSymbols() + { + var index = CreateIndex(("Alpha", ""), ("Beta", "")); + + // "alph" bigrams: "al","lp","ph". "Alpha" has "al","lp","ph","ha". All 3 match. + Assert.True(index.GetTestAccessor().BigramCountCheckPasses("alph")); + + // "beta" bigrams: "be","et","ta". "Beta" has "be","et","ta". All 3 match. + Assert.True(index.GetTestAccessor().BigramCountCheckPasses("beta")); + + // "albe" bigrams: "al","lb","be". "al" from Alpha, "be" from Beta. 2 match ≥ 1 → true. + Assert.True(index.GetTestAccessor().BigramCountCheckPasses("albe")); + } + + /// + /// Demonstrates the key scenario motivating the bigram check: same-length patterns that pass + /// the length check but have completely different character content. Without the bigram check, + /// we'd wastefully attempt fuzzy matching against all symbols of the same length. + /// + [Fact] + public void BigramCountCheck_RejectsSameLengthDifferentContent() + { + var index = CreateIndex(("GooBar", ""), ("BazQuux", ""), ("Simple", "")); + + // "Xyzwvq" has length 6, same as "GooBar" and "Simple". Length check passes. + Assert.True(index.GetTestAccessor().LengthCheckPasses("Xyzwvq")); + // But bigram check fails: "xy","yz","zw","wv","vq" — none stored. + Assert.False(index.GetTestAccessor().BigramCountCheckPasses("Xyzwvq")); + + // "Xyzwvq" is mixed case (X then lowercase), one hump 'X'. 'X' is not a hump initial of + // any symbol. Not all-lowercase, so trigram check doesn't apply either. The bigram check + // also fails. With all three checks failing, the document is skipped entirely. + Assert.Equal(PatternMatcherKind.None, index.CouldContainNavigateToMatch("Xyzwvq", null)); + } + + /// + /// Verifies that the bigram bitset correctly handles the "other" bucket for Unicode characters. + /// Two distinct Unicode characters both map to index 37 ("other"), so their bigrams are + /// indistinguishable. Underscore, by contrast, has its own index (36) and is exact. + /// + [Fact] + public void BigramCountCheck_UnicodeFallsBackToOtherBucket() + { + // "α" and "β" are both non-a-z, non-0-9, non-underscore, so they map to "other" (index 37). + var index = CreateIndex(("Gooαβ", "")); + + // "gooαβ" bigrams: "go","oα","αβ". In the bitset, "oα" maps to (o=14)*38+(other=37), + // and "αβ" maps to (other=37)*38+(other=37). Both stored. + Assert.True(index.GetTestAccessor().BigramCountCheckPasses("gooαβ")); + + // "gooγδ" bigrams: "go","oγ","γδ". "oγ" maps to same index as "oα" (both "other"), + // and "γδ" maps to same as "αβ" (both "other,other"). So this is a false positive + // from the "other" bucket — the bitset can't distinguish them. + Assert.True(index.GetTestAccessor().BigramCountCheckPasses("gooγδ")); + } + + /// + /// Verifies that underscore has its own dedicated index (36) and is NOT in the "other" bucket. + /// This means underscore bigrams are exact — "o_" and "oα" map to different bitset positions. + /// + [Fact] + public void BigramCountCheck_UnderscoreHasOwnIndex() + { + // Index with a symbol that contains underscore bigrams: "a_b" → bigrams "a_", "_b". + var index = CreateIndex(("a_b_c", "")); + + // "a_b_" has bigrams "a_","_b","b_","_c" — length 4, k=1, min_shared=1. + // "a_" is stored → passes. + Assert.True(index.GetTestAccessor().BigramCountCheckPasses("a_b_")); + + // "aαbα" has bigrams "aα","αb","bα" — none of these match "a_","_b","b_","_c" + // because underscore (index 36) != other (index 37). min_shared=1, matches=0 → false. + Assert.False(index.GetTestAccessor().BigramCountCheckPasses("aαbα")); + } + + #endregion + + #region ContainerCheck + + // The container char set is an exact FrozenSet (not a bloom filter), so all + // false-case assertions are guaranteed — no false positives to worry about. + + [Theory] + // ═══ System.Collections.Generic ═══ + // Character-parts: "System", "Collections", "Generic" + // Hump initials stored: S, C, G + // + // True: individual hump initials present. + [InlineData("System.Collections.Generic", "S", true)] + [InlineData("System.Collections.Generic", "C", true)] + [InlineData("System.Collections.Generic", "G", true)] + // True: multiple hump initials (all present, order doesn't matter for container). + [InlineData("System.Collections.Generic", "SC", true)] + [InlineData("System.Collections.Generic", "SG", true)] + [InlineData("System.Collections.Generic", "CG", true)] + [InlineData("System.Collections.Generic", "SCG", true)] + [InlineData("System.Collections.Generic", "GC", true)] // order doesn't matter + // + // False: characters that are not hump initials of the container. + [InlineData("System.Collections.Generic", "A", false)] // 'A' not a hump initial + [InlineData("System.Collections.Generic", "B", false)] // 'B' not a hump initial + [InlineData("System.Collections.Generic", "D", false)] // 'D' not a hump initial + [InlineData("System.Collections.Generic", "X", false)] // 'X' not a hump initial + [InlineData("System.Collections.Generic", "Z", false)] // 'Z' not a hump initial + [InlineData("System.Collections.Generic", "W", false)] // 'W' not a hump initial + // False: one present + one absent. + [InlineData("System.Collections.Generic", "SX", false)] // S present, X absent + [InlineData("System.Collections.Generic", "SA", false)] // S present, A absent + [InlineData("System.Collections.Generic", "CZ", false)] // C present, Z absent + [InlineData("System.Collections.Generic", "GW", false)] // G present, W absent + // False: all absent. + [InlineData("System.Collections.Generic", "XYZ", false)] + [InlineData("System.Collections.Generic", "WZ", false)] + [InlineData("System.Collections.Generic", "ABD", false)] + // + // All-lowercase container patterns: only the first character (uppercased) is checked, + // because we can't determine hump boundaries without casing. + [InlineData("System.Collections.Generic", "s", true)] // first char → 'S' present + [InlineData("System.Collections.Generic", "c", true)] // first char → 'C' present + [InlineData("System.Collections.Generic", "g", true)] // first char → 'G' present + [InlineData("System.Collections.Generic", "a", false)] // first char → 'A' not present + [InlineData("System.Collections.Generic", "b", false)] // first char → 'B' not present + [InlineData("System.Collections.Generic", "x", false)] // first char → 'X' not present + [InlineData("System.Collections.Generic", "z", false)] // first char → 'Z' not present + [InlineData("System.Collections.Generic", "sys", true)] // first char → 'S' present + [InlineData("System.Collections.Generic", "col", true)] // first char → 'C' present + [InlineData("System.Collections.Generic", "xyz", false)] // first char → 'X' not present + [InlineData("System.Collections.Generic", "abc", false)] // first char → 'A' not present + // + // ═══ Goo.Bar ═══ + // Character-parts: "Goo", "Bar" + // Hump initials stored: G, B + // + [InlineData("Goo.Bar", "G", true)] + [InlineData("Goo.Bar", "B", true)] + [InlineData("Goo.Bar", "GB", true)] + [InlineData("Goo.Bar", "BG", true)] // order doesn't matter for container + [InlineData("Goo.Bar", "GA", false)] // G present, A absent + [InlineData("Goo.Bar", "GX", false)] // G present, X absent + [InlineData("Goo.Bar", "A", false)] // 'A' not a hump initial + [InlineData("Goo.Bar", "X", false)] // 'X' not a hump initial + [InlineData("Goo.Bar", "XY", false)] // neither X nor Y are hump initials + // + // ═══ Microsoft.CodeAnalysis.CSharp ═══ + // Character-parts: "Microsoft", "Code", "Analysis", "CSharp" + // Hump initials stored: M, C, A (C appears twice but is deduplicated) + // + [InlineData("Microsoft.CodeAnalysis.CSharp", "MCA", true)] + [InlineData("Microsoft.CodeAnalysis.CSharp", "MC", true)] + [InlineData("Microsoft.CodeAnalysis.CSharp", "MA", true)] + [InlineData("Microsoft.CodeAnalysis.CSharp", "CA", true)] + [InlineData("Microsoft.CodeAnalysis.CSharp", "M", true)] + [InlineData("Microsoft.CodeAnalysis.CSharp", "A", true)] + [InlineData("Microsoft.CodeAnalysis.CSharp", "C", true)] + [InlineData("Microsoft.CodeAnalysis.CSharp", "MX", false)] // M present, X absent + [InlineData("Microsoft.CodeAnalysis.CSharp", "B", false)] // 'B' not a hump initial + [InlineData("Microsoft.CodeAnalysis.CSharp", "X", false)] // 'X' not a hump initial + [InlineData("Microsoft.CodeAnalysis.CSharp", "XZ", false)] // neither present + // + // ═══ GooBarBaz ═══ (single segment, multi-hump — not dotted) + // Character-parts: "Goo", "Bar", "Baz" + // Hump initials stored: G, B (B appears twice but is deduplicated) + // + [InlineData("GooBarBaz", "GB", true)] + [InlineData("GooBarBaz", "G", true)] + [InlineData("GooBarBaz", "B", true)] + [InlineData("GooBarBaz", "A", false)] // 'A' not a hump initial + [InlineData("GooBarBaz", "C", false)] // 'C' not a hump initial + [InlineData("GooBarBaz", "X", false)] // 'X' not a hump initial + [InlineData("GooBarBaz", "GX", false)] // G present, X absent + [InlineData("GooBarBaz", "GA", false)] // G present, A absent + public void ContainerCheck(string symbolContainer, string containerPattern, bool expected) + { + // Use a dummy symbol name; we're testing the container set. + var index = CreateIndex(("Dummy", symbolContainer)); + Assert.Equal(expected, index.GetTestAccessor().ContainerCheckPasses(containerPattern)); + } + + #endregion + + #region ProbablyContainsMatch — Positive (filter must return true) + + /// + /// Tests that returns + /// for patterns that should match, and verifies that the + /// produces the expected match kind. + /// + [Theory] + // ── Exact ── + [InlineData("GooBar", "", "GooBar", null, PatternMatchKind.Exact)] + [InlineData("GooBar", "", "goobar", null, PatternMatchKind.Exact)] + // ── Prefix ── + [InlineData("GooBar", "", "Goo", null, PatternMatchKind.Prefix)] + [InlineData("GooBar", "", "goo", null, PatternMatchKind.Prefix)] + [InlineData("GooBar", "", "G", null, PatternMatchKind.Prefix)] + [InlineData("GooBar", "", "g", null, PatternMatchKind.Prefix)] + [InlineData("GooBarQuux", "", "GooBar", null, PatternMatchKind.Prefix)] + [InlineData("XMLDocument", "", "XMLDoc", null, PatternMatchKind.Prefix)] + [InlineData("XMLDocument", "", "xmldoc", null, PatternMatchKind.Prefix)] + // ── CamelCaseExact (mixed-case pattern) ── + [InlineData("GooBar", "", "GB", null, PatternMatchKind.CamelCaseExact)] + [InlineData("GooBar", "", "GoBa", null, PatternMatchKind.CamelCaseExact)] + [InlineData("GooBarQuux", "", "GBQ", null, PatternMatchKind.CamelCaseExact)] + [InlineData("GooBarQuux", "", "GoBarQu", null, PatternMatchKind.CamelCaseExact)] + [InlineData("NonEmptyList", "", "NEL", null, PatternMatchKind.CamelCaseExact)] + [InlineData("CodeFixProvider", "", "CFP", null, PatternMatchKind.CamelCaseExact)] + [InlineData("XMLDocument", "", "XD", null, PatternMatchKind.CamelCaseExact)] + // ── CamelCaseExact (all-lowercase pattern) ── + [InlineData("GooBar", "", "gb", null, PatternMatchKind.CamelCaseExact)] + [InlineData("GooBar", "", "goba", null, PatternMatchKind.CamelCaseExact)] + [InlineData("GooBarQuux", "", "gbq", null, PatternMatchKind.CamelCaseExact)] + [InlineData("NonEmptyList", "", "nel", null, PatternMatchKind.CamelCaseExact)] + [InlineData("NonEmptyList", "", "neli", null, PatternMatchKind.CamelCaseExact)] + [InlineData("CodeFixProvider", "", "cofipro", null, PatternMatchKind.CamelCaseExact)] + // ── CamelCasePrefix (mixed-case) ── + [InlineData("GooBarQuux", "", "GB", null, PatternMatchKind.CamelCasePrefix)] + [InlineData("GooBarQuux", "", "GoBa", null, PatternMatchKind.CamelCasePrefix)] + [InlineData("GooBarQuux", "", "GoBar", null, PatternMatchKind.CamelCasePrefix)] + [InlineData("CodeFixProviderService", "", "CFP", null, PatternMatchKind.CamelCasePrefix)] + // ── CamelCasePrefix (all-lowercase) ── + [InlineData("GooBarQuux", "", "gb", null, PatternMatchKind.CamelCasePrefix)] + [InlineData("GooBarQuux", "", "goba", null, PatternMatchKind.CamelCasePrefix)] + [InlineData("CodeFixProviderService", "", "cfp", null, PatternMatchKind.CamelCasePrefix)] + // ── CamelCaseNonContiguousPrefix (mixed-case) ── + [InlineData("GooBarQuux", "", "GQ", null, PatternMatchKind.CamelCaseNonContiguousPrefix)] + [InlineData("GooBarQuux", "", "GoQu", null, PatternMatchKind.CamelCaseNonContiguousPrefix)] + // ── CamelCaseNonContiguousPrefix (all-lowercase) ── + [InlineData("GooBarQuux", "", "gq", null, PatternMatchKind.CamelCaseNonContiguousPrefix)] + [InlineData("GooBarQuux", "", "goqu", null, PatternMatchKind.CamelCaseNonContiguousPrefix)] + // ── CamelCaseSubstring (mixed-case) ── + [InlineData("CodeFixProviderService", "", "FP", null, PatternMatchKind.CamelCaseSubstring)] + // ── CamelCaseSubstring (all-lowercase) ── + [InlineData("CodeFixProviderService", "", "fp", null, PatternMatchKind.CamelCaseSubstring)] + // ── CamelCaseNonContiguousSubstring (mixed-case) ── + [InlineData("CodeFixProviderService", "", "FS", null, PatternMatchKind.CamelCaseNonContiguousSubstring)] + // ── CamelCaseNonContiguousSubstring (all-lowercase) ── + [InlineData("CodeFixProviderService", "", "fs", null, PatternMatchKind.CamelCaseNonContiguousSubstring)] + // ── StartOfWordSubstring ── + [InlineData("OperatorBinary", "", "Binary", null, PatternMatchKind.StartOfWordSubstring)] + [InlineData("OperatorBinary", "", "binary", null, PatternMatchKind.StartOfWordSubstring)] + [InlineData("OperatorBinary", "", "Bin", null, PatternMatchKind.StartOfWordSubstring)] + [InlineData("OperatorBinary", "", "bin", null, PatternMatchKind.StartOfWordSubstring)] + // ── LowercaseSubstring ── + [InlineData("Readline", "", "line", null, PatternMatchKind.LowercaseSubstring)] + [InlineData("Combine", "", "bin", null, PatternMatchKind.LowercaseSubstring)] + // ── Fuzzy ── + [InlineData("GooBar", "", "GozBar", null, PatternMatchKind.Fuzzy)] + // ── Container matching ── + [InlineData("Quux", "Goo.Bar", "Qu", "Ba", PatternMatchKind.Prefix)] + [InlineData("Quux", "Goo.Bar", "Qu", "Go.Ba", PatternMatchKind.Prefix)] + [InlineData("Quux", "Goo.Bar.Baz", "Qu", "Ba.Baz", PatternMatchKind.Prefix)] + [InlineData("BazQuux", "GooBar", "BQ", "GB", PatternMatchKind.CamelCaseExact)] + [InlineData("Quux", "GooBar", "Qu", "gb", PatternMatchKind.Prefix)] + internal void ProbablyContainsMatch_Positive( + string symbolName, string symbolContainer, + string patternName, string? patternContainer, + PatternMatchKind expectedNameMatchKind) + { + var index = CreateIndex((symbolName, symbolContainer)); + + var matchKinds = index.CouldContainNavigateToMatch(patternName, patternContainer); + Assert.NotEqual(PatternMatcherKind.None, matchKinds); + + // All these test cases have simple patterns, so non-fuzzy checks pass. + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Standard)); + + // Fuzzy is enabled only when BOTH length check AND bigram count check pass. + var expectedFuzzy = index.GetTestAccessor().LengthCheckPasses(patternName) + && index.GetTestAccessor().BigramCountCheckPasses(patternName); + Assert.Equal(expectedFuzzy, matchKinds.HasFlag(PatternMatcherKind.Fuzzy)); + + var nameMatch = GetNameMatch(symbolName, patternName); + Assert.NotNull(nameMatch); + Assert.Equal(expectedNameMatchKind, nameMatch.Value.Kind); + + if (patternContainer != null) + { + Assert.True(GetContainerMatch(symbolContainer, patternContainer)); + } + } + + #endregion + + #region ProbablyContainsMatch — Negative (filter must return false) + + [Theory] + // ── Name mismatches: pattern hump characters are absent from symbol humps ── + [InlineData("GooBar", "", "Xyz", null)] + [InlineData("GooBar", "", "GBQ", null)] + [InlineData("GooBar", "", "XY", null)] + [InlineData("GooBar", "", "XZ", null)] + [InlineData("GooBar", "", "ZZ", null)] + // ── Container mismatches: name matches but container fails ── + [InlineData("Quux", "Goo.Bar", "Qu", "Xyz")] + [InlineData("Quux", "Goo.Bar", "Qu", "Go.Qu")] + // ── Both name and container mismatch ── + [InlineData("Quux", "Goo.Bar", "XY", "Xyz")] + public void ProbablyContainsMatch_Negative( + string symbolName, string symbolContainer, + string patternName, string? patternContainer) + { + var index = CreateIndex((symbolName, symbolContainer)); + + Assert.Equal(PatternMatcherKind.None, index.CouldContainNavigateToMatch(patternName, patternContainer)); + + if (patternContainer == null) + { + var nameMatch = GetNameMatch(symbolName, patternName); + Assert.Null(nameMatch); + } + } + + #endregion + + #region CrossHumpSubstring — Not Guaranteed + + /// + /// Documents that (cross-hump substring) + /// matches are found by the PatternMatcher but are intentionally NOT targeted by the prefilter. + /// The prefilter has no specific logic for these patterns; however, it may incidentally return + /// true via other checks (e.g. "ooBa" for "GooBar" passes the fuzzy length check because + /// len 4 vs 6 is within ±2). We intentionally do NOT assert on the prefilter result here -- + /// whether it returns true or false for these cases is an implementation detail, not a contract. + /// If we ever want to drop support for this match kind entirely, the prefilter cleanly enables + /// that: just stop searching symbols for documents where only the length check passed. + /// + [Theory] + [InlineData("GooBar", "ooBa", PatternMatchKind.NonLowercaseSubstring)] + [InlineData("GooBar", "oBa", PatternMatchKind.NonLowercaseSubstring)] + [InlineData("OperatorBinary", "torBin", PatternMatchKind.NonLowercaseSubstring)] + internal void CrossHumpSubstring_NotGuaranteed( + string symbolName, string patternName, + PatternMatchKind actualPatternMatcherKind) + { + var nameMatch = GetNameMatch(symbolName, patternName); + Assert.NotNull(nameMatch); + Assert.Equal(actualPatternMatcherKind, nameMatch.Value.Kind); + } + + #endregion + + #region Multiple Symbols + + [Fact] + public void ProbablyContainsMatch_MultipleSymbols_MatchesAny() + { + var index = CreateIndex( + ("Alpha", ""), + ("GooBar", ""), + ("Quux", "System.Collections")); + + Assert.NotEqual(PatternMatcherKind.None, index.CouldContainNavigateToMatch("GB", null)); + Assert.NotEqual(PatternMatcherKind.None, index.CouldContainNavigateToMatch("Al", null)); + Assert.NotEqual(PatternMatcherKind.None, index.CouldContainNavigateToMatch("Qu", "Co")); + + // "XyzXyzXyzXyz" matches nothing (long enough to escape the ±2 length check too). + Assert.Equal(PatternMatcherKind.None, index.CouldContainNavigateToMatch("XyzXyzXyzXyz", null)); + } + + [Fact] + public void ProbablyContainsMatch_EmptyDocument() + { + var index = CreateIndex(); + Assert.Equal(PatternMatcherKind.None, index.CouldContainNavigateToMatch("anything", null)); + } + + #endregion + + #region Bigram selectivity with multiple symbols + + /// + /// Demonstrates that bigram-based hump checks prevent false positives from + /// characters contributed by different symbols in the same document. Since the + /// hump data is in an exact FrozenSet<T>, these rejections are guaranteed. + /// + [Fact] + public void HumpCheck_MultipleSymbols_BigramsPreventCrossSymbolFalsePositives() + { + // "Global" has hump 'G'. "Binary" has hump 'B'. No single symbol has hump pair G→B. + var index = CreateIndex(("Global", ""), ("Binary", "")); + + // Individual chars: both G and B present in the hump set. + Assert.True(index.GetTestAccessor().HumpCheckPasses("G")); + Assert.True(index.GetTestAccessor().HumpCheckPasses("B")); + + // Multi-hump "GB": bigram G→B is NOT stored because no single symbol has hump pair G→B. + Assert.False(index.GetTestAccessor().HumpCheckPasses("GB")); + + // Characters that are not hump initials of any symbol in the document. + Assert.False(index.GetTestAccessor().HumpCheckPasses("A")); + Assert.False(index.GetTestAccessor().HumpCheckPasses("C")); + Assert.False(index.GetTestAccessor().HumpCheckPasses("X")); + Assert.False(index.GetTestAccessor().HumpCheckPasses("Z")); + } + + /// + /// When a symbol has the correct bigram, the check passes even with other symbols present. + /// + [Fact] + public void HumpCheck_MultipleSymbols_CorrectBigramPasses() + { + var index = CreateIndex(("Global", ""), ("Binary", ""), ("GooBar", "")); + + // "GooBar" contributes bigram G→B, so "GB" is stored. + Assert.True(index.GetTestAccessor().HumpCheckPasses("GB")); + + // Bigram B→G is not stored — no symbol has humps in that order. + Assert.False(index.GetTestAccessor().HumpCheckPasses("BG")); + } + + #endregion + + #region Individual checks are independent + + /// + /// Verifies that the hump check, trigram check, and length check operate independently. + /// A pattern that fails one check can pass through another. + /// + [Fact] + public void IndividualChecks_AreIndependent() + { + var index = CreateIndex(("Readline", "")); + + // "line" — fails hump check (no hump 'L' in "Readline"? Actually 'R' is the only hump) + // Wait: "Readline" has character parts: "Readline" (one part since 'R' followed by lowercase). + // So hump initial is just 'R'. Let's verify: + + // Hump check: "line" is all-lowercase, first char 'L'. 'L' is not a hump initial of "Readline" (only 'R'). + // So hump check fails. + Assert.False(index.GetTestAccessor().HumpCheckPasses("line")); + + // Trigram check: "line" has trigrams "lin", "ine". Both stored from "Readline". Passes. + Assert.True(index.GetTestAccessor().TrigramCheckPasses("line")); + + // Length check: "line" has length 4, threshold ±1, checks lengths 3..5. + // "Readline" has length 8, not in range. Fails. + Assert.False(index.GetTestAccessor().LengthCheckPasses("line")); + + // Overall: CouldContainNavigateToMatch returns Standard (trigram check saves it). + // Fuzzy is disabled because the length check failed. + var matchKinds = index.CouldContainNavigateToMatch("line", null); + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Standard)); + Assert.False(matchKinds.HasFlag(PatternMatcherKind.Fuzzy)); + } + + /// + /// A fuzzy match that only passes via the length check. + /// + [Fact] + public void LengthCheck_EnablesFuzzyMatch() + { + var index = CreateIndex(("GooBar", "")); + + // "GozBar" is a fuzzy match (edit distance 1, same length 6). + // Hump check: "GozBar" mixed-case, parts ["Goz","Bar"], humps G, B → bigram "GB" → passes. + // But let's test with a pattern where hump/trigram don't help. + + // "Goxxar" — mixed-case, parts ["Goxxar"] (one part: G followed by lowercase). + // Single hump 'G' → hump check passes trivially. + // So let's use a pattern where hump check fails: + + // "Xoobar" — mixed case? No, 'X' upper then all lower → one part, hump 'X'. 'X' not stored. + Assert.False(index.GetTestAccessor().HumpCheckPasses("Xoobar")); + // Trigram: "Xoobar" is not all-lowercase (capital X) → false. + Assert.False(index.GetTestAccessor().TrigramCheckPasses("Xoobar")); + // Length: "Xoobar" length 6 matches "GooBar" length 6. Within ±2 (threshold for length 6). Passes. + Assert.True(index.GetTestAccessor().LengthCheckPasses("Xoobar")); + // Bigram: "xoobar" bigrams "xo","oo","ob","ba","ar". "GooBar" has "go","oo","ob","ba","ar". + // k=2, min_shared = 6-1-4 = 1. Shared: "oo","ob","ba","ar" = 4 ≥ 1. Passes. + Assert.True(index.GetTestAccessor().BigramCountCheckPasses("Xoobar")); + } + + #endregion + + #region End-to-end: pre-filter → PatternMatcher integration + + /// + /// Verifies that a verbatim identifier pattern like "@static" correctly produces an Exact match + /// against the symbol "static". The pre-filter strips the leading '@' before running hump/trigram + /// checks, so NonFuzzy is correctly set. + /// + [Fact] + public void EndToEnd_VerbatimIdentifierPattern_ProducesExactMatch() + { + var index = CreateIndex(("static", "")); + + var matchKinds = index.CouldContainNavigateToMatch("@static", null); + + // Non-fuzzy passes because stripping '@' → "static" passes hump check. + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Standard)); + // Fuzzy also enabled because length/bigram checks pass. + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Fuzzy)); + + // Verify the PatternMatcher finds the Exact match. + using var matcher = PatternMatcher.CreatePatternMatcher("@static", includeMatchedSpans: false); + var match = matcher.GetFirstMatch("static"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + } + + /// + /// Verifies that a multi-word pattern like "get word" (with a space) correctly matches + /// symbols. The pre-filter splits at spaces and checks each word — "get" passes the hump + /// check because hump 'G' exists, so NonFuzzy is set. + /// + [Fact] + public void EndToEnd_SpaceInPattern_ProducesSubstringMatch() + { + var index = CreateIndex(("get_key_word", ""), ("GetKeyWord", "")); + + var matchKinds = index.CouldContainNavigateToMatch("get word", null); + + // Non-fuzzy checks pass because splitting at space → "get" passes hump check (hump 'G' exists). + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Standard)); + + // Verify the PatternMatcher finds a non-fuzzy match. + using var matcher = PatternMatcher.CreatePatternMatcher("get word", includeMatchedSpans: false); + var match = matcher.GetFirstMatch("get_key_word"); + Assert.NotNull(match); + Assert.NotEqual(PatternMatchKind.Fuzzy, match.Value.Kind); + } + + /// + /// Verifies that "line" vs "Readline" produces a + /// match (all-lowercase pattern at a non-word-boundary). NavigateTo maps this to + /// NavigateToMatchKind.Fuzzy because there is no dedicated NavigateToMatchKind + /// for lowercase substrings — Fuzzy is the closest available quality tier. + /// The pre-filter correctly sets NonFuzzy (trigram check passes for "lin"/"ine"). + /// + [Fact] + public void EndToEnd_LowercaseSubstring_MapsToFuzzyInNavigateTo() + { + var index = CreateIndex(("Readline", "")); + + var matchKinds = index.CouldContainNavigateToMatch("line", null); + + // Standard set via trigram check. Fuzzy not set (length 4 vs 8, delta 4 > ±2). + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Standard)); + Assert.False(matchKinds.HasFlag(PatternMatcherKind.Fuzzy)); + + // PatternMatcher returns LowercaseSubstring, NOT Substring. + // "line" is all-lowercase and matches at position 4 in "Readline" (not at a word boundary). + using var matcher = PatternMatcher.CreatePatternMatcher("line", includeMatchedSpans: false); + var match = matcher.GetFirstMatch("Readline"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.LowercaseSubstring, match.Value.Kind); + } + + /// + /// When hump/trigram fail even after preprocessing, fuzzy is enabled by the length + bigram checks. + /// "Xoobar" shares no hump structure or trigrams with "GooBar", but same length and enough shared + /// bigrams → fuzzy match. + /// + [Fact] + public void EndToEnd_OnlyFuzzyChecksPasses_FuzzyMatchUsed() + { + var index = CreateIndex(("GooBar", "")); + + Assert.False(index.GetTestAccessor().HumpCheckPasses("Xoobar")); + Assert.False(index.GetTestAccessor().TrigramCheckPasses("Xoobar")); + Assert.True(index.GetTestAccessor().LengthCheckPasses("Xoobar")); + // "xoobar" bigrams: "xo","oo","ob","ba","ar". "GooBar" bigrams: "go","oo","ob","ba","ar". + // k=2, min_shared=1, matches=4 → passes. + Assert.True(index.GetTestAccessor().BigramCountCheckPasses("Xoobar")); + + var matchKinds = index.CouldContainNavigateToMatch("Xoobar", null); + Assert.False(matchKinds.HasFlag(PatternMatcherKind.Standard)); + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Fuzzy)); + + // Non-fuzzy matcher won't find a match. + using var standardMatcher = PatternMatcher.CreatePatternMatcher("Xoobar", includeMatchedSpans: false); + Assert.Null(standardMatcher.GetFirstMatch("GooBar")); + + // Fuzzy matcher finds it. + using var fuzzyMatcher = PatternMatcher.CreatePatternMatcher("Xoobar", includeMatchedSpans: false, PatternMatcherKind.Fuzzy); + var match = fuzzyMatcher.GetFirstMatch("GooBar"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Fuzzy, match.Value.Kind); + } + + /// + /// Mirrors the VB NavigateTo test "TestFindVerbatimClass": searching for "class" (all-lowercase, + /// length 5) against a symbol named "Class" (length 5) must produce an Exact match, not Fuzzy. + /// The non-fuzzy pass in the PatternMatcher should find this as a case-insensitive exact match + /// before the fuzzy pass is even attempted. + /// + [Fact] + public void EndToEnd_CaseInsensitiveExact_Length5_ProducesExactNotFuzzy() + { + var index = CreateIndex(("Class", "")); + + var matchKinds = index.CouldContainNavigateToMatch("class", null); + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Standard)); + + using var matcher = PatternMatcher.CreatePatternMatcher("class", includeMatchedSpans: false, matchKinds); + var match = matcher.GetFirstMatch("Class"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + } + + /// + /// Mirrors the VB NavigateTo test "TestFindVerbatimClass" for the "[class]" search. + /// In VB, brackets are used to escape keywords as identifiers. The pattern "[class]" should + /// strip brackets as punctuation, leaving "class" which matches "Class" as Exact. + /// + [Fact] + public void EndToEnd_BracketedPattern_ProducesExactNotFuzzy() + { + var index = CreateIndex(("Class", "")); + + var matchKinds = index.CouldContainNavigateToMatch("[class]", null); + + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Standard), $"Expected Standard flag but got: {matchKinds}"); + + using var matcher = PatternMatcher.CreatePatternMatcher("[class]", includeMatchedSpans: false, matchKinds); + var match = matcher.GetFirstMatch("Class"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + } + + /// + /// Underscores are valid word characters and must NOT be stripped by the pre-filter. + /// A pattern like "_myField" should match symbols with that name via standard (non-fuzzy) matching. + /// + [Fact] + public void EndToEnd_UnderscorePattern_PreservesUnderscore() + { + var index = CreateIndex(("_myField", "")); + + var matchKinds = index.CouldContainNavigateToMatch("_myField", null); + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Standard), $"Expected Standard flag but got: {matchKinds}"); + + using var matcher = PatternMatcher.CreatePatternMatcher("_myField", includeMatchedSpans: false, matchKinds); + var match = matcher.GetFirstMatch("_myField"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + } + + /// + /// Underscores surrounded by brackets (e.g., "[_class]" in VB) should strip the brackets + /// but preserve the underscore, matching "_class" as Exact. + /// + [Fact] + public void EndToEnd_BracketedUnderscorePattern_PreservesUnderscore() + { + var index = CreateIndex(("_class", "")); + + var matchKinds = index.CouldContainNavigateToMatch("[_class]", null); + Assert.True(matchKinds.HasFlag(PatternMatcherKind.Standard), $"Expected Standard flag but got: {matchKinds}"); + + using var matcher = PatternMatcher.CreatePatternMatcher("[_class]", includeMatchedSpans: false, matchKinds); + var match = matcher.GetFirstMatch("_class"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + } + + #endregion +} diff --git a/src/Workspaces/CoreTest/FindSymbols/RegexPatternMatcherTests.cs b/src/Workspaces/CoreTest/FindSymbols/RegexPatternMatcherTests.cs new file mode 100644 index 000000000000..753063cf7da7 --- /dev/null +++ b/src/Workspaces/CoreTest/FindSymbols/RegexPatternMatcherTests.cs @@ -0,0 +1,362 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.PatternMatching; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.FindSymbols; + +public class RegexPatternMatcherTests +{ + private static PatternMatch? GetMatch(string pattern, string candidate, bool includeMatchedSpans = false) + { + using var matcher = PatternMatcher.CreateNameMatcher(pattern, isRegex: true, includeMatchedSpans); + if (matcher is null) + return null; + + using var matches = TemporaryArray.Empty; + if (matcher.AddMatches(candidate, ref matches.AsRef())) + return matches[0]; + + return null; + } + + #region Positive — matching + + [Fact] + public void SimpleLiteral_ExactMatch() + { + var match = GetMatch("ReadLine", "ReadLine"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void SimpleLiteral_CaseInsensitiveMatch() + { + var match = GetMatch("readline", "ReadLine"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + Assert.False(match.Value.IsCaseSensitive); + } + + [Fact] + public void Alternation_FirstBranch() + { + var match = GetMatch("(Read|Write)Line", "ReadLine"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void Alternation_SecondBranch() + { + var match = GetMatch("(Read|Write)Line", "WriteLine"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void SubstringMatch() + { + var match = GetMatch("Read", "StreamReader"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.NonLowercaseSubstring, match.Value.Kind); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void DotStar_SubstringMatch() + { + var match = GetMatch("Goo.*Bar", "GooSomethingBar"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void DotStar_PartialMatch() + { + var match = GetMatch("Goo.*Bar", "MyGooSomethingBarEnd"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.NonLowercaseSubstring, match.Value.Kind); + } + + [Fact] + public void AnchoredExact() + { + var match = GetMatch("^ReadLine$", "ReadLine"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + } + + [Fact] + public void CharacterClass() + { + var match = GetMatch("[A-Z]ead", "Read"); + Assert.NotNull(match); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void MatchedSpans_Reported() + { + var match = GetMatch("Line", "ReadLine", includeMatchedSpans: true); + Assert.NotNull(match); + Assert.Single(match.Value.MatchedSpans); + Assert.Equal(4, match.Value.MatchedSpans[0].Start); + Assert.Equal(4, match.Value.MatchedSpans[0].Length); + } + + [Fact] + public void MatchedSpans_RegexSubstring() + { + var match = GetMatch("Goo.*Bar", "MyGooSomethingBarEnd", includeMatchedSpans: true); + Assert.NotNull(match); + Assert.Single(match.Value.MatchedSpans); + // "GooSomethingBar" starts at index 2, length 15 + Assert.Equal(2, match.Value.MatchedSpans[0].Start); + Assert.Equal(15, match.Value.MatchedSpans[0].Length); + } + + [Fact] + public void MatchedSpans_ExactMatch_CoversFullString() + { + var match = GetMatch("GooBar", "GooBar", includeMatchedSpans: true); + Assert.NotNull(match); + Assert.Single(match.Value.MatchedSpans); + Assert.Equal(0, match.Value.MatchedSpans[0].Start); + Assert.Equal(6, match.Value.MatchedSpans[0].Length); + } + + [Fact] + public void MatchedSpans_NotReported_WhenNotRequested() + { + var match = GetMatch("Line", "ReadLine", includeMatchedSpans: false); + Assert.NotNull(match); + Assert.True(match.Value.MatchedSpans.IsDefaultOrEmpty); + } + + [Fact] + public void Quantifiers_OneOrMore() + { + var match = GetMatch("Go+Bar", "GooBar"); + Assert.NotNull(match); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void Quantifiers_ZeroOrMore() + { + var match = GetMatch("Go*Bar", "GBar"); + Assert.NotNull(match); + } + + [Fact] + public void EscapedDot_LiteralMatch() + { + var match = GetMatch(@"Goo\.Bar", "Goo.Bar"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + } + + [Fact] + public void CaseInsensitive_SubstringMatch() + { + var match = GetMatch("read", "StreamReader"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.NonLowercaseSubstring, match.Value.Kind); + Assert.False(match.Value.IsCaseSensitive); + } + + [Fact] + public void CaseSensitive_MatchHigherPriority() + { + var match = GetMatch("Read", "ReadLine"); + Assert.NotNull(match); + Assert.True(match.Value.IsCaseSensitive); + } + + #endregion + + #region Negative — no match + + [Fact] + public void NoMatch_DifferentText() + { + var match = GetMatch("ReadLine", "WriteBuffer"); + Assert.Null(match); + } + + [Fact] + public void NoMatch_AnchoredPrefix_NotFull() + { + var match = GetMatch("^ReadLine$", "MyReadLine"); + Assert.Null(match); + } + + [Fact] + public void NoMatch_AlternationMiss() + { + var match = GetMatch("(Read|Write)Line", "StreamBuffer"); + Assert.Null(match); + } + + [Fact] + public void NoMatch_EmptyCandidate() + { + var match = GetMatch("Goo", ""); + Assert.Null(match); + } + + [Fact] + public void NoMatch_CaseMattersForAnchored() + { + var match = GetMatch("^readLine$", "ReadLine"); + Assert.NotNull(match); + Assert.False(match.Value.IsCaseSensitive); + } + + #endregion + + #region Case sensitivity categorization + + [Fact] + public void CaseSensitivity_ExactCaseSensitive() + { + var match = GetMatch("GooBar", "GooBar"); + Assert.NotNull(match); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void CaseSensitivity_ExactCaseInsensitive() + { + var match = GetMatch("goobar", "GooBar"); + Assert.NotNull(match); + Assert.False(match.Value.IsCaseSensitive); + } + + [Fact] + public void CaseSensitivity_SubstringCaseSensitive() + { + var match = GetMatch("Goo", "GooBar"); + Assert.NotNull(match); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void CaseSensitivity_SubstringCaseInsensitive() + { + var match = GetMatch("goo", "GooBar"); + Assert.NotNull(match); + Assert.False(match.Value.IsCaseSensitive); + } + + [Fact] + public void CaseSensitivity_Alternation_MixedCase() + { + var match = GetMatch("Read|write", "ReadLine"); + Assert.NotNull(match); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void CaseSensitivity_Alternation_CaseInsensitiveOnly() + { + var match = GetMatch("read|write", "ReadLine"); + Assert.NotNull(match); + Assert.False(match.Value.IsCaseSensitive); + } + + #endregion + + #region Edge cases + + [Fact] + public void SingleCharPattern() + { + var match = GetMatch("R", "ReadLine"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.NonLowercaseSubstring, match.Value.Kind); + } + + [Fact] + public void WildcardOnly_MatchesAnything() + { + var match = GetMatch(".*", "Anything"); + Assert.NotNull(match); + } + + [Fact] + public void NullCandidate_NoMatch() + { + var match = GetMatch("Goo", null!); + Assert.Null(match); + } + + [Fact] + public void WhitespaceCandidate_NoMatch() + { + var match = GetMatch("Goo", " "); + Assert.Null(match); + } + + [Fact] + public void InvalidRegex_ReturnsNull() + { + var match = GetMatch("(unclosed", "Anything"); + Assert.Null(match); + } + + [Fact] + public void InvalidRegex_BadEscape_ReturnsNull() + { + var match = GetMatch(@"\", "Anything"); + Assert.Null(match); + } + + #endregion + + #region Whitespace handling + + [Fact] + public void WhitespaceInPattern_IsStripped() + { + var match = GetMatch("( Read | Write ) Line", "ReadLine"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void WhitespaceInPattern_IsStripped_SecondBranch() + { + var match = GetMatch("( Read | Write ) Line", "WriteLine"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + Assert.True(match.Value.IsCaseSensitive); + } + + [Fact] + public void WhitespaceInPattern_SimpleLiteral() + { + var match = GetMatch("Goo Bar", "GooBar"); + Assert.NotNull(match); + Assert.Equal(PatternMatchKind.Exact, match.Value.Kind); + } + + [Fact] + public void WhitespaceInPattern_DoesNotMatchSpacedCandidate() + { + var match = GetMatch("Goo Bar", "Goo Bar"); + Assert.Null(match); + } + + #endregion +} diff --git a/src/Workspaces/CoreTest/FindSymbols/RegexPreFilterTests.cs b/src/Workspaces/CoreTest/FindSymbols/RegexPreFilterTests.cs new file mode 100644 index 000000000000..17136a31284e --- /dev/null +++ b/src/Workspaces/CoreTest/FindSymbols/RegexPreFilterTests.cs @@ -0,0 +1,217 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.PatternMatching; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.FindSymbols; + +public sealed class RegexPreFilterTests +{ + private static NavigateToSearchIndex CreateIndex(params (string name, string container)[] symbols) + { + var infos = symbols.Select(s => CreateInfo(s.name, s.container)).ToImmutableArray(); + return NavigateToSearchIndex.TestAccessor.CreateIndex(infos); + } + + private static DeclaredSymbolInfo CreateInfo(string name, string container) + { + var stringTable = new StringTable(); + return DeclaredSymbolInfo.Create( + stringTable, name, nameSuffix: null, containerDisplayName: null, + fullyQualifiedContainerName: container, isPartial: false, hasAttributes: false, + DeclaredSymbolInfoKind.Class, Accessibility.Public, + default, ImmutableArray.Empty); + } + + #region RegexQueryCheckPasses — positive (passes pre-filter) + + [Fact] + public void LiteralQuery_MatchingBigrams_Passes() + { + var index = CreateIndex(("ReadLine", "")); + var query = new RegexQuery.Literal("readline"); + Assert.True(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void LiteralQuery_SubstringPresent_Passes() + { + var index = CreateIndex(("ReadLine", "")); + var query = new RegexQuery.Literal("read"); + Assert.True(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void AllQuery_BothLiteralsPresent_Passes() + { + var index = CreateIndex(("ReadLine", "")); + var query = new RegexQuery.All([ + new RegexQuery.Literal("read"), + new RegexQuery.Literal("line"), + ]); + Assert.True(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void AnyQuery_OneBranchPresent_Passes() + { + var index = CreateIndex(("ReadLine", "")); + var query = new RegexQuery.Any([ + new RegexQuery.Literal("read"), + new RegexQuery.Literal("write"), + ]); + Assert.True(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void CaseInsensitive_Passes() + { + // RegexQueryCompiler lowercases literals at compile time, so the literal + // arriving here is already lowercase. The index stores lowercased bigrams, + // so the check succeeds. + var index = CreateIndex(("ReadLine", "")); + var query = new RegexQuery.Literal("readline"); + Assert.True(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void ComplexQuery_ReadOrWriteLine_Passes() + { + var index = CreateIndex(("ReadLine", ""), ("WriteLine", "")); + var query = new RegexQuery.All([ + new RegexQuery.Any([ + new RegexQuery.Literal("read"), + new RegexQuery.Literal("write"), + ]), + new RegexQuery.Literal("line"), + ]); + Assert.True(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void MultipleSymbols_BigramsAccumulate() + { + // "Goo" contributes "go","oo" bigrams; "Bar" contributes "ba","ar" + var index = CreateIndex(("Goo", ""), ("Bar", "")); + var query = new RegexQuery.All([ + new RegexQuery.Literal("goo"), + new RegexQuery.Literal("bar"), + ]); + Assert.True(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + #endregion + + #region RegexQueryCheckPasses — negative (rejects document) + + [Fact] + public void LiteralQuery_NoMatchingBigrams_Fails() + { + var index = CreateIndex(("ReadLine", "")); + var query = new RegexQuery.Literal("xyz"); + Assert.False(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void AllQuery_OneChildFails_Fails() + { + var index = CreateIndex(("ReadLine", "")); + var query = new RegexQuery.All([ + new RegexQuery.Literal("read"), + new RegexQuery.Literal("xyz"), + ]); + Assert.False(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void AnyQuery_AllChildrenFail_Fails() + { + var index = CreateIndex(("ReadLine", "")); + var query = new RegexQuery.Any([ + new RegexQuery.Literal("xyz"), + new RegexQuery.Literal("qwerty"), + ]); + Assert.False(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void EmptyIndex_FailsOnLiteral() + { + var index = CreateIndex(); + var query = new RegexQuery.Literal("goo"); + Assert.False(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void TrigramMismatch_Fails() + { + var index = CreateIndex(("xyz", "")); + var query = new RegexQuery.Literal("abc"); + Assert.False(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + #endregion + + #region False-positive baselines + + /// + /// Documents the known false-positive rate of bigram/trigram pre-filtering. + /// These are cases where the pre-filter passes but the regex would not actually match. + /// This establishes a baseline — future improvements (e.g. sparse n-grams) should + /// reduce these false positives. + /// + [Fact] + public void FalsePositive_Baseline_ReorderedBigrams() + { + // "GooBar" has bigrams: go, oo, ob, ba, ar + // Query Literal("bargoo") has bigrams: ba, ar, rg, go, oo + // "rg" is not present, so bigram check should reject this. + var index = CreateIndex(("GooBar", "")); + var query = new RegexQuery.Literal("bargoo"); + Assert.False(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void FalsePositive_Baseline_SharedBigramsAcrossSymbols() + { + // Two symbols "Goo" and "Bar" together contribute bigrams: go, oo, ba, ar + // Query Literal("ooba") has bigrams: oo, ob, ba + // "ob" is not from either "Goo" or "Bar" individually, but bigrams accumulate + // across symbols. "Goo" has 'o' and "Bar" has 'b' but the bigram "ob" is only + // present if some symbol has those two chars adjacent. + var index = CreateIndex(("Goo", ""), ("Bar", "")); + var query = new RegexQuery.Literal("ooba"); + Assert.False(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void FalsePositive_Baseline_PermutedTrigrams() + { + // Document has "abcdef" — trigrams: abc, bcd, cde, def + // Query Literal("defabc") has trigrams: def, efa, fab, abc + // "efa" and "fab" are not present → correctly rejected. + var index = CreateIndex(("abcdef", "")); + var query = new RegexQuery.Literal("defabc"); + Assert.False(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + [Fact] + public void FalsePositive_Baseline_BigramCollision() + { + // The bigram bitset maps non-ASCII chars to an "other" bucket (index 37). + // Two different non-ASCII chars may collide, causing a false positive. + // "αβ" maps to (37,37). If we search for "γδ" it also maps to (37,37) → false positive. + var index = CreateIndex(("αβ", "")); + var query = new RegexQuery.Literal("γδ"); + Assert.True(index.GetTestAccessor().RegexQueryCheckPasses(query)); + } + + #endregion +} diff --git a/src/Workspaces/CoreTest/Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj b/src/Workspaces/CoreTest/Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj index df55ccc847f1..b0c8e607e880 100644 --- a/src/Workspaces/CoreTest/Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj +++ b/src/Workspaces/CoreTest/Microsoft.CodeAnalysis.Workspaces.UnitTests.csproj @@ -4,7 +4,7 @@ Library - $(NetVSShared);net472 + $(NetRoslynWindowsTests);net472 Microsoft.CodeAnalysis.UnitTests diff --git a/src/Workspaces/CoreTest/SolutionTests/MetadataServiceTests.cs b/src/Workspaces/CoreTest/SolutionTests/MetadataServiceTests.cs new file mode 100644 index 000000000000..acccf166d58b --- /dev/null +++ b/src/Workspaces/CoreTest/SolutionTests/MetadataServiceTests.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Test.Utilities; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests; + +[UseExportProvider] +public sealed class MetadataServiceTests : TestBase +{ + [Fact] + public void GetReference_ValidAssembly_ReturnsPortableExecutableReference() + { + using var workspace = SolutionTestHelpers.CreateWorkspace(); + var metadataService = workspace.Services.GetRequiredService(); + + var properties = MetadataReferenceProperties.Assembly.WithAliases(["global", "MyAlias"]).WithEmbedInteropTypes(true); + + var mscorlibPath = typeof(object).Assembly.Location; + var reference = metadataService.GetReference(mscorlibPath, properties); + + Assert.NotNull(reference); + Assert.Equal(mscorlibPath, reference.FilePath); + Assert.Equal(properties, reference.Properties); + + Assert.NotNull(reference.GetMetadata()); + } + + [Fact] + public void GetReference_SamePathAndProperties_ReturnsCachedReference() + { + using var workspace = SolutionTestHelpers.CreateWorkspace(); + var metadataService = workspace.Services.GetRequiredService(); + + var mscorlibPath = typeof(object).Assembly.Location; + var reference1 = metadataService.GetReference(mscorlibPath, MetadataReferenceProperties.Assembly); + var reference2 = metadataService.GetReference(mscorlibPath, MetadataReferenceProperties.Assembly); + + Assert.Same(reference1, reference2); + } + + [Fact] + public void GetReference_NonExistentFile_ReturnsThrowingReference() + { + using var workspace = SolutionTestHelpers.CreateWorkspace(); + var metadataService = workspace.Services.GetRequiredService(); + + var nonExistentPath = Path.Combine(TempRoot.Root, Guid.NewGuid().ToString() + ".dll"); + var reference1 = metadataService.GetReference(nonExistentPath, MetadataReferenceProperties.Assembly); + var reference2 = metadataService.GetReference(nonExistentPath, MetadataReferenceProperties.Assembly); + + // Failure is cached: + Assert.Same(reference1, reference2); + + // Reference is returned even for non-existent files + Assert.NotNull(reference1); + Assert.Equal(nonExistentPath, reference1.FilePath); + + // Accessing metadata should throw the stored IOException + Assert.Throws(reference1.GetMetadata); + } +} diff --git a/src/Workspaces/CoreTestUtilities/Logging/TestOutputLoggerProvider.cs b/src/Workspaces/CoreTestUtilities/Logging/TestOutputLoggerProvider.cs index e16cf3cc1f3a..27eac37f9d2b 100644 --- a/src/Workspaces/CoreTestUtilities/Logging/TestOutputLoggerProvider.cs +++ b/src/Workspaces/CoreTestUtilities/Logging/TestOutputLoggerProvider.cs @@ -51,6 +51,18 @@ public void Dispose() } } + /// + /// Validates that this provider has not already been disposed, and then disposes it. This is useful in + /// test teardown to catch bugs where the provider was disposed too early and we might lose messages. + /// + public void ValidateNotAlreadyDisposedAndDispose() + { + if (_testOutputHelper is null) + throw new ObjectDisposedException(nameof(TestOutputLoggerProvider)); + + Dispose(); + } + public void Dispose() { _testOutputHelper = null; diff --git a/src/Workspaces/CoreTestUtilities/Microsoft.CodeAnalysis.Workspaces.Test.Utilities.csproj b/src/Workspaces/CoreTestUtilities/Microsoft.CodeAnalysis.Workspaces.Test.Utilities.csproj index 890e10ebe2a2..3da3fa38c07f 100644 --- a/src/Workspaces/CoreTestUtilities/Microsoft.CodeAnalysis.Workspaces.Test.Utilities.csproj +++ b/src/Workspaces/CoreTestUtilities/Microsoft.CodeAnalysis.Workspaces.Test.Utilities.csproj @@ -5,7 +5,7 @@ Library Microsoft.CodeAnalysis.UnitTests - $(NetVSShared);net472 + $(NetRoslynAll);net472 false true true @@ -65,6 +65,7 @@ + diff --git a/src/Workspaces/CoreTestUtilities/Workspaces/TestSourceTextContainer.cs b/src/Workspaces/CoreTestUtilities/Workspaces/TestSourceTextContainer.cs new file mode 100644 index 000000000000..6be3986a4d5d --- /dev/null +++ b/src/Workspaces/CoreTestUtilities/Workspaces/TestSourceTextContainer.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Test.Utilities; + +internal sealed class TestSourceTextContainer : SourceTextContainer +{ + public required SourceText Text { get; init; } + + public override SourceText CurrentText => Text; + +#pragma warning disable CS0067 // The event 'TestSourceTextContainer.TextChanged' is never used + public override event EventHandler? TextChanged; +#pragma warning restore CS0067 +} diff --git a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs index 6b52acce1885..606e45cd0d94 100644 --- a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs +++ b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. @@ -339,6 +339,9 @@ public override bool CanApplyChange(ApplyChangesKind feature) case ApplyChangesKind.RemoveDocument: return KindSupportsAddRemoveDocument(); + case ApplyChangesKind.RemoveProject: + return KindSupportsRemoveProject(); + case ApplyChangesKind.AddAdditionalDocument: case ApplyChangesKind.RemoveAdditionalDocument: case ApplyChangesKind.AddAnalyzerConfigDocument: @@ -373,6 +376,15 @@ private bool KindSupportsAddRemoveDocument() _ => true }; + private bool KindSupportsRemoveProject() + => _workspaceKind switch + { + WorkspaceKind.MiscellaneousFiles => false, + WorkspaceKind.Interactive => false, + WorkspaceKind.SemanticSearch => false, + _ => true + }; + protected override void ApplyDocumentAdded(DocumentInfo info, SourceText text) { var hostProject = this.GetTestProject(info.Id.ProjectId); diff --git a/src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs b/src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs index 78e07460731a..238b6ffdcecf 100644 --- a/src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs +++ b/src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs @@ -57,6 +57,8 @@ internal sealed class ProjectBuildManager { PropertyNames.ShouldUnsetParentConfigurationAndPlatform, bool.FalseString } }.ToImmutableDictionary(); + public ImmutableArray KnownCommandLineParserLanguages { get; } + private readonly ImmutableDictionary _additionalGlobalProperties; private readonly ILogger? _msbuildLogger; private MSB.Evaluation.ProjectCollection? _batchBuildProjectCollection; @@ -70,8 +72,9 @@ internal sealed class ProjectBuildManager } } - public ProjectBuildManager(ImmutableDictionary additionalGlobalProperties, ILogger? msbuildLogger = null) + public ProjectBuildManager(ImmutableArray knownCommandLineParserLanguages, ImmutableDictionary additionalGlobalProperties, ILogger? msbuildLogger = null) { + KnownCommandLineParserLanguages = knownCommandLineParserLanguages; _additionalGlobalProperties = additionalGlobalProperties ?? ImmutableDictionary.Empty; _msbuildLogger = msbuildLogger; } diff --git a/src/Workspaces/MSBuild/BuildHost/BuildHost.cs b/src/Workspaces/MSBuild/BuildHost/BuildHost.cs index 9a08ed2537a5..b2a05948f277 100644 --- a/src/Workspaces/MSBuild/BuildHost/BuildHost.cs +++ b/src/Workspaces/MSBuild/BuildHost/BuildHost.cs @@ -29,6 +29,11 @@ internal sealed class BuildHost : IBuildHost /// private ImmutableDictionary? _globalMSBuildProperties; + /// + /// Should not be changed once the is initialized. + /// + private ImmutableArray _knownCommandLineParserLanguages; + /// /// The binary log path to use for all builds; should not be changed once the is initialized. /// @@ -141,7 +146,7 @@ private void CreateBuildManager() _logger.LogInformation($"Logging builds to {_binaryLogPath}"); } - _buildManager = new ProjectBuildManager(_globalMSBuildProperties, logger); + _buildManager = new ProjectBuildManager(_knownCommandLineParserLanguages, _globalMSBuildProperties, logger); _buildManager.StartBatchBuild(_globalMSBuildProperties); } } @@ -161,7 +166,7 @@ private void EnsureMSBuildLoaded(string projectFilePath) Contract.ThrowIfFalse(TryEnsureMSBuildLoaded(projectFilePath), $"We don't have an MSBuild to use; {nameof(HasUsableMSBuild)} should have been called first to check."); } - public void ConfigureGlobalState(ImmutableDictionary globalProperties, string? binlogPath) + public void ConfigureGlobalState(ImmutableArray knownCommandLineParserLanguages, ImmutableDictionary globalProperties, string? binlogPath) { lock (_gate) { @@ -170,6 +175,7 @@ public void ConfigureGlobalState(ImmutableDictionary globalPrope _globalMSBuildProperties = globalProperties; _binaryLogPath = binlogPath; + _knownCommandLineParserLanguages = knownCommandLineParserLanguages; } } @@ -230,15 +236,7 @@ private int LoadProjectCore(string projectFilePath, string projectContent, strin private int AddProjectFileTarget(Build.Evaluation.Project? project, string languageName, DiagnosticLog log) { Contract.ThrowIfNull(_buildManager); - - var projectFile = new ProjectFile( - languageName, - project, - ProjectCommandLineProvider.Create(languageName), - _buildManager, - log); - - return _server.AddTarget(projectFile); + return _server.AddTarget(new ProjectFile(languageName, project, _buildManager, log)); } public Task TryGetProjectOutputPathAsync(string projectFilePath, CancellationToken cancellationToken) diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/Constants/PropertyNames.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/Constants/PropertyNames.cs index c879e44f72e2..eea6df08addf 100644 --- a/src/Workspaces/MSBuild/BuildHost/MSBuild/Constants/PropertyNames.cs +++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/Constants/PropertyNames.cs @@ -72,4 +72,5 @@ internal static class PropertyNames public const string WarningsAsErrors = nameof(WarningsAsErrors); public const string WarningsNotAsErrors = nameof(WarningsNotAsErrors); public const string ChecksumAlgorithm = nameof(ChecksumAlgorithm); + public const string PdbChecksumAlgorithm = nameof(PdbChecksumAlgorithm); } diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/CommandLineArgumentReader.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/CommandLineArgumentReader.cs index c51a03469fd0..0f8692b742b2 100644 --- a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/CommandLineArgumentReader.cs +++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/CommandLineArgumentReader.cs @@ -112,18 +112,6 @@ protected void AddWithPlusOrMinus(string name, bool condition) } } - protected string GetDocumentFilePath(MSB.Framework.ITaskItem documentItem) - { - return GetAbsolutePath(documentItem.ItemSpec); - } - - protected string GetAbsolutePath(string path) - { - var baseDirectory = PathUtilities.GetDirectoryName(Project.FullPath); - var absolutePath = FileUtilities.ResolveRelativePath(path, baseDirectory) ?? path; - return FileUtilities.TryNormalizeAbsolutePath(absolutePath) ?? absolutePath; - } - protected void ReadAdditionalFiles() { var additionalFiles = Project.GetAdditionalFiles(); @@ -131,7 +119,7 @@ protected void ReadAdditionalFiles() { foreach (var additionalFile in additionalFiles) { - Add("additionalfile", GetDocumentFilePath(additionalFile)); + Add("additionalfile", Project.GetAbsolutePath(additionalFile.ItemSpec)); } } } @@ -143,14 +131,14 @@ protected void ReadAnalyzers() { foreach (var analyzer in analyzers) { - Add("analyzer", GetDocumentFilePath(analyzer)); + Add("analyzer", Project.GetAbsolutePath(analyzer.ItemSpec)); } } } protected void ReadCodePage() { - var codePage = Project.ReadPropertyInt(PropertyNames.CodePage); + var codePage = Project.ReadCodePage(); AddIfTrue("codepage", codePage.ToString(), codePage != 0); } @@ -238,33 +226,23 @@ protected void ReadPlatform() protected void ReadReferences() { - var references = Project.GetMetadataReferences(); - if (references != null) + foreach (var (filePath, aliases) in Project.GetMetadataReferences()) { - foreach (var reference in references) + if (aliases.IsEmpty) + { + Add("reference", filePath); + } + else { - if (reference.ReferenceOutputAssemblyIsTrue()) + foreach (var alias in aliases) { - var filePath = GetDocumentFilePath(reference); - - var aliases = reference.GetAliases(); - if (aliases.IsDefaultOrEmpty) + if (string.Equals(alias, "global", StringComparison.OrdinalIgnoreCase)) { Add("reference", filePath); } else { - foreach (var alias in aliases) - { - if (string.Equals(alias, "global", StringComparison.OrdinalIgnoreCase)) - { - Add("reference", filePath); - } - else - { - Add("reference", $"{alias}=\"{filePath}\""); - } - } + Add("reference", $"{alias}=\"{filePath}\""); } } } diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Conversions.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Conversions.cs index 57944a9b2e9b..e413b84705b1 100644 --- a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Conversions.cs +++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Conversions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Globalization; namespace Microsoft.CodeAnalysis.MSBuild; @@ -24,7 +25,7 @@ public static int ToInt(string? value) } else { - if (int.TryParse(value, out var result)) + if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return result; } diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Extensions.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Extensions.cs index 17852954070e..cd8df8531413 100644 --- a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Extensions.cs +++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/Extensions.cs @@ -7,6 +7,7 @@ using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; using MSB = Microsoft.Build; namespace Microsoft.CodeAnalysis.MSBuild; @@ -25,24 +26,39 @@ internal static class Extensions public static IEnumerable GetEditorConfigFiles(this MSB.Execution.ProjectInstance executedProject) => executedProject.GetItems(ItemNames.EditorConfigFiles); - public static IEnumerable GetMetadataReferences(this MSB.Execution.ProjectInstance executedProject) - => executedProject.GetItems(ItemNames.ReferencePath); + public static IEnumerable GetMetadataReferences(this MSB.Execution.ProjectInstance project) + { + foreach (var item in project.GetItems(ItemNames.ReferencePath)) + { + if (item.ReferenceOutputAssemblyIsTrue()) + { + yield return new(project.GetAbsolutePath(item.EvaluatedInclude), item.GetAliases()); + } + } + } + + public static string GetAbsolutePath(this MSB.Execution.ProjectInstance project, string path) + { + var baseDirectory = PathUtilities.GetDirectoryName(project.FullPath); + var absolutePath = FileUtilities.ResolveRelativePath(path, baseDirectory) ?? path; + return FileUtilities.TryNormalizeAbsolutePath(absolutePath) ?? absolutePath; + } public static IEnumerable GetProjectReferences(this MSB.Execution.ProjectInstance executedProject) => executedProject .GetItems(ItemNames.ProjectReference) .Select(CreateProjectFileReference); - public static ImmutableArray GetPackageReferences(this MSB.Execution.ProjectInstance executedProject) + public static ImmutableArray GetPackageReferences(this MSB.Execution.ProjectInstance executedProject) { var packageReferenceItems = executedProject.GetItems(ItemNames.PackageReference); - using var _ = PooledHashSet.GetInstance(out var references); + using var _ = PooledHashSet.GetInstance(out var references); foreach (var item in packageReferenceItems) { var name = item.EvaluatedInclude; var versionRangeValue = item.GetMetadataValue(MetadataNames.Version); - var packageReference = new PackageReference(name, versionRangeValue); + var packageReference = new PackageReferenceItem(name, versionRangeValue); references.Add(packageReference); } @@ -81,6 +97,9 @@ public static bool ReadPropertyBool(this MSB.Execution.ProjectInstance executedP public static int ReadPropertyInt(this MSB.Execution.ProjectInstance executedProject, string propertyName) => Conversions.ToInt(executedProject.ReadPropertyString(propertyName)); + public static int ReadCodePage(this MSB.Execution.ProjectInstance executedProject) + => executedProject.ReadPropertyInt(PropertyNames.CodePage) is >= 0 and var codePage ? codePage : 0; + public static ulong ReadPropertyULong(this MSB.Execution.ProjectInstance executedProject, string propertyName) => Conversions.ToULong(executedProject.ReadPropertyString(propertyName)); diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectCommandLineReader.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectCommandLineReader.cs index 3272d99e3686..5fbc77c3447b 100644 --- a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectCommandLineReader.cs +++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectCommandLineReader.cs @@ -17,11 +17,18 @@ internal abstract class ProjectCommandLineProvider public abstract IEnumerable GetCompilerCommandLineArgs(MSB.Execution.ProjectInstance executedProject); public abstract ImmutableArray ReadCommandLineArgs(MSB.Execution.ProjectInstance project); - public static ProjectCommandLineProvider Create(string languageName) - => languageName switch + public static ProjectCommandLineProvider? TryCreate(string languageName, ImmutableArray knownCommandLineParserLanguages) + { + if (!knownCommandLineParserLanguages.Contains(languageName)) + { + return null; + } + + return languageName switch { LanguageNames.CSharp => CSharpProjectCommandLineProvider.Instance, LanguageNames.VisualBasic => VisualBasicProjectCommandLineProvider.Instance, - _ => throw ExceptionUtilities.UnexpectedValue(languageName) + _ => null, }; + } } diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectFile.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectFile.cs index c5011d181209..fc49785e3dff 100644 --- a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectFile.cs +++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectFile.cs @@ -17,10 +17,11 @@ namespace Microsoft.CodeAnalysis.MSBuild; internal sealed class ProjectFile( string language, MSB.Evaluation.Project? project, - ProjectCommandLineProvider commandLineProvider, ProjectBuildManager buildManager, DiagnosticLog log) : IProjectFile { + private readonly ProjectCommandLineProvider? _commandLineProvider = ProjectCommandLineProvider.TryCreate(language, buildManager.KnownCommandLineParserLanguages); + public string FilePath => project?.FullPath ?? string.Empty; @@ -42,7 +43,7 @@ public async Task> GetProjectFileInfosAsync(Canc var projectInstances = await buildManager.BuildProjectInstancesAsync(project, log, cancellationToken).ConfigureAwait(false); return projectInstances.SelectAsArray( - instance => new ProjectInstanceReader(commandLineProvider, instance, project).CreateProjectFileInfo()); + instance => new ProjectInstanceReader(language, _commandLineProvider, instance, project).CreateProjectFileInfo()); } public void AddDocument(string filePath, string? logicalPath = null) diff --git a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstanceReader.cs b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstanceReader.cs index 145d6fd82c77..4d8a5a3a4ce4 100644 --- a/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstanceReader.cs +++ b/src/Workspaces/MSBuild/BuildHost/MSBuild/ProjectFile/ProjectInstanceReader.cs @@ -14,17 +14,21 @@ namespace Microsoft.CodeAnalysis.MSBuild; internal readonly struct ProjectInstanceReader { - private readonly ProjectCommandLineProvider _commandLineProvider; + private readonly ProjectCommandLineProvider? _commandLineProvider; public readonly MSB.Evaluation.Project? Project; public readonly MSB.Execution.ProjectInstance _projectInstance; private readonly string _projectDirectory; + public string Language { get; } + public ProjectInstanceReader( - ProjectCommandLineProvider commandLineReader, + string language, + ProjectCommandLineProvider? commandLineReader, MSB.Execution.ProjectInstance projectInstance, MSB.Evaluation.Project? project) { + Language = language; _commandLineProvider = commandLineReader; _projectInstance = projectInstance; Project = project; @@ -34,12 +38,9 @@ public ProjectInstanceReader( public string FilePath => _projectInstance.FullPath; - public string Language - => _commandLineProvider.Language; - public ProjectFileInfo CreateProjectFileInfo() { - var commandLineArgs = GetCommandLineArgs(_projectInstance); + var commandLineArgs = TryGetCommandLineArgs(_projectInstance); var outputFilePath = _projectInstance.ReadPropertyString(PropertyNames.TargetPath); if (!RoslynString.IsNullOrWhiteSpace(outputFilePath)) @@ -95,8 +96,19 @@ public ProjectFileInfo CreateProjectFileInfo() var packageReferences = _projectInstance.GetPackageReferences(); + // Do not pass metadata references if we have command line args that already specify them. + var metadataReferences = commandLineArgs.IsEmpty ? _projectInstance.GetMetadataReferences().ToImmutableArray() : []; + var projectCapabilities = _projectInstance.GetItems(ItemNames.ProjectCapability).SelectAsArray(item => item.ToString()); var contentFileInfo = GetContentFiles(_projectInstance); + var codePage = _projectInstance.ReadCodePage(); + + var checksumAlgorithm = _projectInstance.ReadPropertyString(PropertyNames.ChecksumAlgorithm); + if (string.IsNullOrEmpty(checksumAlgorithm)) + { + // F# uses PdbChecksumAlgorithm property name + checksumAlgorithm = _projectInstance.ReadPropertyString(PropertyNames.PdbChecksumAlgorithm); + } var fileGlobs = Project?.GetAllGlobs().SelectAsArray(GetFileGlobs) ?? []; @@ -119,6 +131,9 @@ public ProjectFileInfo CreateProjectFileInfo() AnalyzerConfigDocuments = analyzerConfigDocs, ProjectReferences = [.. _projectInstance.GetProjectReferences()], PackageReferences = packageReferences, + MetadataReferences = metadataReferences, + CodePage = codePage, + ChecksumAlgorithm = checksumAlgorithm, ProjectCapabilities = projectCapabilities, ContentFilePaths = contentFileInfo, FileGlobs = fileGlobs @@ -141,8 +156,13 @@ private static ImmutableArray GetContentFiles(MSB.Execution.ProjectInsta return contentFiles; } - private ImmutableArray GetCommandLineArgs(MSB.Execution.ProjectInstance project) + private ImmutableArray TryGetCommandLineArgs(MSB.Execution.ProjectInstance project) { + if (_commandLineProvider == null) + { + return []; + } + var commandLineArgs = _commandLineProvider.GetCompilerCommandLineArgs(project) .SelectAsArray(item => item.ItemSpec); diff --git a/src/Workspaces/MSBuild/Contracts/IBuildHost.cs b/src/Workspaces/MSBuild/Contracts/IBuildHost.cs index 8310712b7a68..0f4a0cce271c 100644 --- a/src/Workspaces/MSBuild/Contracts/IBuildHost.cs +++ b/src/Workspaces/MSBuild/Contracts/IBuildHost.cs @@ -33,7 +33,8 @@ internal interface IBuildHost /// Called once on a new process to configure some global state. This is used for these rather than passing through command line strings, since these contain data that might /// contain paths (which can have escaping issues) or could be quite large (which could run into length limits). /// - void ConfigureGlobalState(ImmutableDictionary globalProperties, string? binlogPath); + /// Languages whose command line parser we understand (ICommandLineParserService). + void ConfigureGlobalState(ImmutableArray knownCommandLineParserLanguages, ImmutableDictionary globalProperties, string? binlogPath); Task LoadProjectFileAsync(string projectFilePath, string languageName, CancellationToken cancellationToken); diff --git a/src/Workspaces/MSBuild/Contracts/MetadataReferenceItem.cs b/src/Workspaces/MSBuild/Contracts/MetadataReferenceItem.cs new file mode 100644 index 000000000000..1019e3f33ab7 --- /dev/null +++ b/src/Workspaces/MSBuild/Contracts/MetadataReferenceItem.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Runtime.Serialization; + +namespace Microsoft.CodeAnalysis.MSBuild; + +[DataContract] +internal readonly record struct MetadataReferenceItem( + [property: DataMember(Order = 0)] string Path, + [property: DataMember(Order = 1)] ImmutableArray Aliases +); diff --git a/src/Workspaces/MSBuild/Contracts/PackageReference.cs b/src/Workspaces/MSBuild/Contracts/PackageReferenceItem.cs similarity index 90% rename from src/Workspaces/MSBuild/Contracts/PackageReference.cs rename to src/Workspaces/MSBuild/Contracts/PackageReferenceItem.cs index b77346803bca..63a1ca97ae21 100644 --- a/src/Workspaces/MSBuild/Contracts/PackageReference.cs +++ b/src/Workspaces/MSBuild/Contracts/PackageReferenceItem.cs @@ -7,7 +7,7 @@ namespace Microsoft.CodeAnalysis.MSBuild; [DataContract] -internal sealed record PackageReference( +internal sealed record PackageReferenceItem( [property: DataMember(Order = 0)] string Name, [property: DataMember(Order = 1)] string VersionRange ); diff --git a/src/Workspaces/MSBuild/Contracts/ProjectFileInfo.cs b/src/Workspaces/MSBuild/Contracts/ProjectFileInfo.cs index 320c754f5b8b..44c76d510015 100644 --- a/src/Workspaces/MSBuild/Contracts/ProjectFileInfo.cs +++ b/src/Workspaces/MSBuild/Contracts/ProjectFileInfo.cs @@ -13,7 +13,7 @@ namespace Microsoft.CodeAnalysis.MSBuild; /// the information from a single target framework. /// [DataContract] -internal sealed class ProjectFileInfo +internal sealed record ProjectFileInfo { [DataMember] public bool IsEmpty { get; init; } @@ -131,7 +131,25 @@ internal sealed class ProjectFileInfo /// Any package references defined on the project. /// [DataMember] - public ImmutableArray PackageReferences { get; init; } + public ImmutableArray PackageReferences { get; init; } + + /// + /// Metadata references referenced by the project if not specified in . + /// + [DataMember] + public ImmutableArray MetadataReferences { get; init; } + + /// + /// The value of `CodePage` property. Zero if not specified. + /// + [DataMember] + public int CodePage { get; init; } + + /// + /// The value of `ChecksumAlgorithm` or `PdbChecksumAlgorithm` property. + /// + [DataMember] + public string? ChecksumAlgorithm { get; init; } /// /// Target framework version (for .net framework projects) @@ -159,6 +177,9 @@ public static ProjectFileInfo CreateEmpty(string language, string? filePath) AnalyzerConfigDocuments = [], ProjectReferences = [], PackageReferences = [], + MetadataReferences = [], + CodePage = 0, + ChecksumAlgorithm = null, ProjectCapabilities = [], ContentFilePaths = [], FileGlobs = [] diff --git a/src/Workspaces/MSBuild/Core/MSBuild/BuildHostProcessManager.cs b/src/Workspaces/MSBuild/Core/MSBuild/BuildHostProcessManager.cs index eeb621f10036..a6bcf58c2ad1 100644 --- a/src/Workspaces/MSBuild/Core/MSBuild/BuildHostProcessManager.cs +++ b/src/Workspaces/MSBuild/Core/MSBuild/BuildHostProcessManager.cs @@ -15,6 +15,7 @@ using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; +using Microsoft.CodeAnalysis.Host; using Microsoft.Extensions.Logging; using Roslyn.Utilities; @@ -22,6 +23,7 @@ namespace Microsoft.CodeAnalysis.MSBuild; internal sealed class BuildHostProcessManager : IAsyncDisposable { + private readonly ImmutableArray _knownCommandLineParserLanguages; private readonly ImmutableDictionary _globalMSBuildProperties; private readonly ILoggerFactory? _loggerFactory; private readonly ILogger? _logger; @@ -35,8 +37,13 @@ internal sealed class BuildHostProcessManager : IAsyncDisposable private static readonly string DotnetExecutable = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "dotnet.exe" : "dotnet"; - public BuildHostProcessManager(ImmutableDictionary? globalMSBuildProperties = null, IBinLogPathProvider? binaryLogPathProvider = null, ILoggerFactory? loggerFactory = null) + public BuildHostProcessManager( + ImmutableArray knownCommandLineParserLanguages, + ImmutableDictionary? globalMSBuildProperties = null, + IBinLogPathProvider? binaryLogPathProvider = null, + ILoggerFactory? loggerFactory = null) { + _knownCommandLineParserLanguages = knownCommandLineParserLanguages; _globalMSBuildProperties = globalMSBuildProperties ?? ImmutableDictionary.Empty; _binaryLogPathProvider = binaryLogPathProvider; _loggerFactory = loggerFactory; @@ -127,7 +134,7 @@ async Task NoLock_GetBuildHostAsync(BuildHostProcessKind build throw new Exception($"The build host was started but we were unable to connect to it's pipe. The process exited with {process.ExitCode}. Process output:{Environment.NewLine}{buildHostProcess.GetBuildHostProcessOutput()}", innerException: e); } - await buildHostProcess.BuildHost.ConfigureGlobalStateAsync(_globalMSBuildProperties, _binaryLogPathProvider?.GetNewLogPath(), cancellationToken).ConfigureAwait(false); + await buildHostProcess.BuildHost.ConfigureGlobalStateAsync(_knownCommandLineParserLanguages, _globalMSBuildProperties, _binaryLogPathProvider?.GetNewLogPath(), cancellationToken).ConfigureAwait(false); if (buildHostKind != BuildHostProcessKind.NetCore || projectOrSolutionFilePath is null diff --git a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.Worker.cs b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.Worker.cs index 4157ab80caa8..8680695b3d54 100644 --- a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.Worker.cs +++ b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.Worker.cs @@ -199,69 +199,37 @@ private async Task CreateProjectInfoAsync(ProjectFileInfo projectFi ? VersionStamp.Default : VersionStamp.Create(FileUtilities.GetFileTimeStamp(projectPath)); - if (projectFileInfo.IsEmpty) + var projectDirectory = Path.GetDirectoryName(projectPath); + var metadataService = _solutionServices.GetRequiredService(); + + IEnumerable resolvedMetadataReferences; + string? assemblyName; + SourceHashAlgorithm checksumAlgorithm; + ParseOptions? parseOptions; + CompilationOptions? compilationOptions; + IEnumerable analyzerReferences; + Encoding? encoding; + + var commandLineParser = _solutionServices.GetLanguageServices(projectFileInfo.Language).GetService(); + if (commandLineParser != null) { - var assemblyName = GetAssemblyNameFromProjectPath(projectPath); - - var parseOptions = GetLanguageService(language) - ?.GetDefaultParseOptions(); - var compilationOptions = GetLanguageService(language) - ?.GetDefaultCompilationOptions(); - - return ProjectInfo.Create( - new ProjectInfo.ProjectAttributes( - projectId, - version, - name: projectName, - assemblyName: assemblyName, - language: language, - compilationOutputInfo: new CompilationOutputInfo(projectFileInfo.IntermediateOutputFilePath, projectFileInfo.GeneratedFilesOutputDirectory), - checksumAlgorithm: SourceHashAlgorithms.Default, - outputFilePath: projectFileInfo.OutputFilePath, - outputRefFilePath: projectFileInfo.OutputRefFilePath, - filePath: projectPath), - compilationOptions: compilationOptions, - parseOptions: parseOptions); - } - - return await _progress.DoOperationAndReportProgressAsync(ProjectLoadOperation.Resolve, projectPath, projectFileInfo.TargetFramework, async () => - { - var projectDirectory = Path.GetDirectoryName(projectPath); - - // parse command line arguments - var commandLineParser = GetLanguageService(projectFileInfo.Language); - - if (commandLineParser is null) - { - var message = string.Format(WorkspaceMSBuildResources.Unable_to_find_a_0_for_1, nameof(ICommandLineParserService), projectFileInfo.Language); - throw new Exception(message); - } - var commandLineArgs = commandLineParser.Parse( arguments: projectFileInfo.CommandLineArgs, baseDirectory: projectDirectory, isInteractive: false, sdkDirectory: RuntimeEnvironment.GetRuntimeDirectory()); - var assemblyName = commandLineArgs.CompilationName; - if (RoslynString.IsNullOrWhiteSpace(assemblyName)) - { - // if there isn't an assembly name, make one from the file path. - // Note: This may not be necessary any longer if the command line args - // always produce a valid compilation name. - assemblyName = GetAssemblyNameFromProjectPath(projectPath); - } + assemblyName = commandLineArgs.CompilationName; // Ensure that doc-comments are parsed - var parseOptions = commandLineArgs.ParseOptions; + parseOptions = commandLineArgs.ParseOptions; if (parseOptions.DocumentationMode == DocumentationMode.None) { parseOptions = parseOptions.WithDocumentationMode(DocumentationMode.Parse); } // add all the extra options that are really behavior overrides - var metadataService = GetWorkspaceService(); - var compilationOptions = commandLineArgs.CompilationOptions + compilationOptions = commandLineArgs.CompilationOptions .WithXmlReferenceResolver(new XmlFileResolver(projectDirectory)) .WithSourceReferenceResolver(new SourceFileResolver([], projectDirectory)) // TODO: https://github.com/dotnet/roslyn/issues/4967 @@ -269,39 +237,68 @@ private async Task CreateProjectInfoAsync(ProjectFileInfo projectFi .WithStrongNameProvider(new DesktopStrongNameProvider(commandLineArgs.KeyFileSearchPaths, Path.GetTempPath())) .WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default); - var documents = CreateDocumentInfos(projectFileInfo.Documents, projectId, commandLineArgs.Encoding); - var additionalDocuments = CreateDocumentInfos(projectFileInfo.AdditionalDocuments, projectId, commandLineArgs.Encoding); - var analyzerConfigDocuments = CreateDocumentInfos(projectFileInfo.AnalyzerConfigDocuments, projectId, commandLineArgs.Encoding); - CheckForDuplicateDocuments(documents.Concat(additionalDocuments).Concat(analyzerConfigDocuments), projectPath, projectId); - - var analyzerReferences = ResolveAnalyzerReferences(commandLineArgs); - - var resolvedReferences = await ResolveReferencesAsync(projectId, projectFileInfo, commandLineArgs, cancellationToken).ConfigureAwait(false); - - return ProjectInfo.Create( - new ProjectInfo.ProjectAttributes( - projectId, - version, - projectName, - assemblyName, - language, - compilationOutputInfo: new CompilationOutputInfo(projectFileInfo.IntermediateOutputFilePath, projectFileInfo.GeneratedFilesOutputDirectory), - checksumAlgorithm: commandLineArgs.ChecksumAlgorithm, - filePath: projectPath, - outputFilePath: projectFileInfo.OutputFilePath, - outputRefFilePath: projectFileInfo.OutputRefFilePath, - isSubmission: false), - compilationOptions: compilationOptions, - parseOptions: parseOptions, - documents: documents, - projectReferences: resolvedReferences.ProjectReferences, - metadataReferences: resolvedReferences.MetadataReferences, - analyzerReferences: analyzerReferences, - additionalDocuments: additionalDocuments, - hostObjectType: null) - .WithDefaultNamespace(projectFileInfo.DefaultNamespace) - .WithAnalyzerConfigDocuments(analyzerConfigDocuments); - }).ConfigureAwait(false); + encoding = commandLineArgs.Encoding; + + analyzerReferences = ResolveAnalyzerReferences(commandLineArgs); + + resolvedMetadataReferences = commandLineArgs.ResolveMetadataReferences( + new WorkspaceMetadataFileReferenceResolver( + metadataService, + new RelativePathResolver(commandLineArgs.ReferencePaths, commandLineArgs.BaseDirectory))); + + checksumAlgorithm = commandLineArgs.ChecksumAlgorithm; + } + else + { + assemblyName = null; + parseOptions = null; + compilationOptions = null; + analyzerReferences = []; + + encoding = EncodedStringText.TryGetCodePageEncoding(projectFileInfo.CodePage); + + checksumAlgorithm = !string.IsNullOrEmpty(projectFileInfo.ChecksumAlgorithm) && SourceHashAlgorithms.TryParseAlgorithmName(projectFileInfo.ChecksumAlgorithm, out var algorithm) + ? algorithm : SourceHashAlgorithms.Default; + + resolvedMetadataReferences = projectFileInfo.MetadataReferences + .Select(r => metadataService.GetReference(r.Path, new MetadataReferenceProperties(aliases: r.Aliases))); + } + + var documents = CreateDocumentInfos(projectFileInfo.Documents, projectId, encoding); + var additionalDocuments = CreateDocumentInfos(projectFileInfo.AdditionalDocuments, projectId, encoding); + var analyzerConfigDocuments = CreateDocumentInfos(projectFileInfo.AnalyzerConfigDocuments, projectId, encoding); + + CheckForDuplicateDocuments(documents.Concat(additionalDocuments).Concat(analyzerConfigDocuments), projectPath, projectId); + + var resolvedReferences = await _progress.DoOperationAndReportProgressAsync( + ProjectLoadOperation.Resolve, + projectPath, + projectFileInfo.TargetFramework, + () => ResolveReferencesAsync(projectId, projectFileInfo, resolvedMetadataReferences, cancellationToken)).ConfigureAwait(false); + + return ProjectInfo.Create( + new ProjectInfo.ProjectAttributes( + projectId, + version, + projectName, + assemblyName ?? GetAssemblyNameFromProjectPath(projectPath), + language, + compilationOutputInfo: new CompilationOutputInfo(projectFileInfo.IntermediateOutputFilePath, projectFileInfo.GeneratedFilesOutputDirectory), + checksumAlgorithm: checksumAlgorithm, + filePath: projectPath, + outputFilePath: projectFileInfo.OutputFilePath, + outputRefFilePath: projectFileInfo.OutputRefFilePath, + isSubmission: false), + compilationOptions: compilationOptions, + parseOptions: parseOptions, + documents: documents, + projectReferences: resolvedReferences.ProjectReferences, + metadataReferences: resolvedReferences.MetadataReferences, + analyzerReferences: analyzerReferences, + additionalDocuments: additionalDocuments, + hostObjectType: null) + .WithDefaultNamespace(projectFileInfo.DefaultNamespace) + .WithAnalyzerConfigDocuments(analyzerConfigDocuments); } private static string GetAssemblyNameFromProjectPath(string? projectFilePath) @@ -409,12 +406,6 @@ private void CheckForDuplicateDocuments(ImmutableArray documents, } } - private TLanguageService? GetLanguageService(string languageName) - where TLanguageService : ILanguageService - => _solutionServices - .GetLanguageServices(languageName) - .GetService(); - private TWorkspaceService? GetWorkspaceService() where TWorkspaceService : IWorkspaceService => _solutionServices diff --git a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.Worker_ResolveReferences.cs b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.Worker_ResolveReferences.cs index 347874c13517..b8e83f226c73 100644 --- a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.Worker_ResolveReferences.cs +++ b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.Worker_ResolveReferences.cs @@ -184,14 +184,8 @@ public ResolvedReferences ToResolvedReferences() => new(GetProjectReferences(), GetMetadataReferences()); } - private async Task ResolveReferencesAsync(ProjectId id, ProjectFileInfo projectFileInfo, CommandLineArguments commandLineArgs, CancellationToken cancellationToken) + private async Task ResolveReferencesAsync(ProjectId id, ProjectFileInfo projectFileInfo, IEnumerable resolvedMetadataReferences, CancellationToken cancellationToken) { - // First, gather all of the metadata references from the command-line arguments. - var resolvedMetadataReferences = commandLineArgs.ResolveMetadataReferences( - new WorkspaceMetadataFileReferenceResolver( - metadataService: _solutionServices.GetRequiredService(), - pathResolver: new RelativePathResolver(commandLineArgs.ReferencePaths, commandLineArgs.BaseDirectory))); - var builder = new ResolvedReferencesBuilder(resolvedMetadataReferences); var projectDirectory = PathUtilities.GetDirectoryName(projectFileInfo.FilePath); diff --git a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs index ebd926895708..b2de2cd87541 100644 --- a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs +++ b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs @@ -23,6 +23,7 @@ public partial class MSBuildProjectLoader { // the services for the projects and solutions are intended to be loaded into. private readonly SolutionServices _solutionServices; + private readonly ImmutableArray _knownCommandLineParserLanguages; private readonly DiagnosticReporter _diagnosticReporter; private readonly Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory; @@ -38,10 +39,11 @@ internal MSBuildProjectLoader( ImmutableDictionary? properties) { _solutionServices = solutionServices; + _knownCommandLineParserLanguages = solutionServices.GetSupportedLanguages(); _diagnosticReporter = diagnosticReporter; _loggerFactory = new Microsoft.Extensions.Logging.LoggerFactory([new DiagnosticReporterLoggerProvider(_diagnosticReporter)]); _pathResolver = new PathResolver(_diagnosticReporter); - _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(solutionServices, diagnosticReporter); + _projectFileExtensionRegistry = new ProjectFileExtensionRegistry(diagnosticReporter); Properties = ImmutableDictionary.Create(StringComparer.OrdinalIgnoreCase); @@ -257,7 +259,7 @@ private async Task> LoadInfoAsync( ? new BinLogPathProvider(fileName) : null; - var buildHostProcessManager = new BuildHostProcessManager(Properties, binLogPathProvider, _loggerFactory); + var buildHostProcessManager = new BuildHostProcessManager(_knownCommandLineParserLanguages, Properties, binLogPathProvider, _loggerFactory); await using var _ = buildHostProcessManager.ConfigureAwait(false); var projectFileProvider = new BuildHostProjectFileInfoProvider( diff --git a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildWorkspace.cs b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildWorkspace.cs index 6d28a0428cb2..50f12f3f2661 100644 --- a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildWorkspace.cs +++ b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildWorkspace.cs @@ -31,12 +31,15 @@ public sealed class MSBuildWorkspace : Workspace private readonly MSBuildProjectLoader _loader; + private readonly ImmutableArray _knownCommandLineParserLanguages; + private MSBuildWorkspace( HostServices hostServices, ImmutableDictionary properties) : base(hostServices, WorkspaceKind.MSBuild) { _loader = new MSBuildProjectLoader(this, properties); + _knownCommandLineParserLanguages = Services.SolutionServices.GetSupportedLanguages(); } /// @@ -93,6 +96,13 @@ private DiagnosticReporter Reporter internal void AddLoggerProvider(Microsoft.Extensions.Logging.ILoggerProvider loggerProvider) => _loader.LoggerFactory.AddProvider(loggerProvider); + protected override void Dispose(bool finalize) + { + // Dispose the LoggerFactory to ensure any logger providers added via AddLoggerProvider are disposed. + _loader.LoggerFactory.Dispose(); + base.Dispose(finalize); + } + /// /// The MSBuild properties used when interpreting project files. /// These are the same properties that are passed to msbuild via the /property:<n>=<v> command line argument. @@ -127,8 +137,7 @@ public bool LoadMetadataForReferencedProjects /// An project is unrecognized if it either has /// a) an invalid file path, /// b) a non-existent project file, - /// c) has an unrecognized file extension or - /// d) a file extension associated with an unsupported language. + /// c) has an unrecognized file extension /// /// If unrecognized projects cannot be skipped a corresponding exception is thrown. /// @@ -313,7 +322,7 @@ internal override bool TryApplyChanges(Solution newSolution, IProgress _extensionToLanguageMap; private readonly NonReentrantLock _dataGuard; - public ProjectFileExtensionRegistry(SolutionServices solutionServices, DiagnosticReporter diagnosticReporter) + public ProjectFileExtensionRegistry(DiagnosticReporter diagnosticReporter) { - _solutionServices = solutionServices; _diagnosticReporter = diagnosticReporter; + _extensionToLanguageMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "csproj", LanguageNames.CSharp }, - { "vbproj", LanguageNames.VisualBasic } + { "vbproj", LanguageNames.VisualBasic }, + { "fsproj", LanguageNames.FSharp } }; _dataGuard = new NonReentrantLock(); @@ -58,27 +58,11 @@ public bool TryGetLanguageNameFromProjectPath(string? projectFilePath, Diagnosti if (extension is ['.', .. var rest]) extension = rest; - if (_extensionToLanguageMap.TryGetValue(extension, out var language)) - { - if (_solutionServices.SupportedLanguages.Contains(language) && - _solutionServices.GetLanguageServices(language).GetService() is not null) - { - languageName = language; - return true; - } - else - { - _diagnosticReporter.Report(mode, string.Format(WorkspacesResources.Cannot_open_project_0_because_the_language_1_is_not_supported, projectFilePath, language)); - languageName = null; - return false; - } - } - else - { - _diagnosticReporter.Report(mode, string.Format(WorkspacesResources.Cannot_open_project_0_because_the_file_extension_1_is_not_associated_with_a_language, projectFilePath, Path.GetExtension(projectFilePath))); - languageName = null; - return false; - } + if (_extensionToLanguageMap.TryGetValue(extension, out languageName)) + return true; + + _diagnosticReporter.Report(mode, string.Format(WorkspacesResources.Cannot_open_project_0_because_the_file_extension_1_is_not_associated_with_a_language, projectFilePath, Path.GetExtension(projectFilePath))); + return false; } } } diff --git a/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs b/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs index 72df5327bc66..a7e79b9e3521 100644 --- a/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs +++ b/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs @@ -39,9 +39,9 @@ public RemoteBuildHost(RpcClient client) public Task HasUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken) => _client.InvokeAsync(BuildHostTargetObject, nameof(IBuildHost.HasUsableMSBuild), parameters: [projectOrSolutionFilePath], cancellationToken); - /// - public Task ConfigureGlobalStateAsync(ImmutableDictionary globalProperties, string? binlogPath, CancellationToken cancellationToken) - => _client.InvokeAsync(BuildHostTargetObject, nameof(IBuildHost.ConfigureGlobalState), parameters: [globalProperties, binlogPath], cancellationToken); + /// + public Task ConfigureGlobalStateAsync(ImmutableArray knownCommandLineParserLanguages, ImmutableDictionary globalProperties, string? binlogPath, CancellationToken cancellationToken) + => _client.InvokeAsync(BuildHostTargetObject, nameof(IBuildHost.ConfigureGlobalState), parameters: [knownCommandLineParserLanguages, globalProperties, binlogPath], cancellationToken); public async Task LoadProjectFileAsync(string projectFilePath, string languageName, CancellationToken cancellationToken) { diff --git a/src/Workspaces/MSBuild/Test/MSBuildWorkspaceTestBase.cs b/src/Workspaces/MSBuild/Test/MSBuildWorkspaceTestBase.cs index a94c9a22054d..48f2eb6b4df2 100644 --- a/src/Workspaces/MSBuild/Test/MSBuildWorkspaceTestBase.cs +++ b/src/Workspaces/MSBuild/Test/MSBuildWorkspaceTestBase.cs @@ -24,13 +24,31 @@ namespace Microsoft.CodeAnalysis.MSBuild.UnitTests; public abstract class MSBuildWorkspaceTestBase : WorkspaceTestBase { - protected readonly ITestOutputHelper TestOutput; + private readonly ITestOutputHelper _testOutputHelper; + private readonly TestOutputLoggerProvider _testOutputLoggerProvider; protected readonly ILoggerFactory LoggerFactory; protected MSBuildWorkspaceTestBase(ITestOutputHelper testOutput) { - TestOutput = testOutput; - LoggerFactory = new LoggerFactory([new TestOutputLoggerProvider(testOutput)]); + _testOutputHelper = testOutput; + _testOutputLoggerProvider = new TestOutputLoggerProvider(testOutput); + LoggerFactory = new LoggerFactory([_testOutputLoggerProvider]); + } + + public override void Dispose() + { + // Dispose our LoggingFactory and providers. xunit validates that we don't write anything to ITestOutputHelper after a test is done, + // so we want to ensure our providers are all disposed so a broken test doesn't cause the entire test run to fail -- our implementation of + // TestOutputLoggerProvider stops forwarding messages once it's disposed. + // + // LoggerFactory's handling of lifetime is subtle -- providers passed to the LoggerFactorys' constructor are not owned and we have to dispose them; + // but providers added via AddLoggerProvider are owned and will be disposed by the LoggerFactory. Thus we need to dispose both here. + LoggerFactory.Dispose(); + + // We'll call ValidateNotAlreadyDisposedAndDispose() as a way to ensure this wasn't disposed prematurely, which would cause us to lose log output. + _testOutputLoggerProvider.ValidateNotAlreadyDisposedAndDispose(); + + base.Dispose(); } protected const string MSBuildNamespace = "http://schemas.microsoft.com/developer/msbuild/2003"; @@ -160,7 +178,7 @@ protected MSBuildWorkspace CreateMSBuildWorkspace( { additionalProperties ??= []; var workspace = MSBuildWorkspace.Create(CreateProperties(additionalProperties)); - workspace.AddLoggerProvider(new TestOutputLoggerProvider(TestOutput)); + workspace.AddLoggerProvider(new TestOutputLoggerProvider(_testOutputHelper)); if (throwOnWorkspaceFailed) { diff --git a/src/Workspaces/MSBuild/Test/Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests.csproj b/src/Workspaces/MSBuild/Test/Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests.csproj index 3130fb3db871..2d76973db1ad 100644 --- a/src/Workspaces/MSBuild/Test/Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests.csproj +++ b/src/Workspaces/MSBuild/Test/Microsoft.CodeAnalysis.Workspaces.MSBuild.UnitTests.csproj @@ -4,7 +4,7 @@ Library Microsoft.CodeAnalysis.MSBuild.UnitTests - $(NetRoslyn);net472 + $(NetRoslynWindowsTests);net472 diff --git a/src/Workspaces/MSBuild/Test/NetCoreTests.cs b/src/Workspaces/MSBuild/Test/NetCoreTests.cs index 10610b67b563..169f3acf9dbc 100644 --- a/src/Workspaces/MSBuild/Test/NetCoreTests.cs +++ b/src/Workspaces/MSBuild/Test/NetCoreTests.cs @@ -14,6 +14,7 @@ using Microsoft.Build.Logging; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.UnitTests; using Microsoft.CodeAnalysis.UnitTests.TestFiles; using Microsoft.CodeAnalysis.VisualBasic; @@ -131,7 +132,7 @@ public async Task TestOpenInMemoryProject_NetCoreApp() File.Delete(projectFilePath); var projectDir = Path.GetDirectoryName(projectFilePath); - await using var buildHostProcessManager = new BuildHostProcessManager(ImmutableDictionary.Empty); + await using var buildHostProcessManager = new BuildHostProcessManager([LanguageNames.CSharp], ImmutableDictionary.Empty); var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None); var projectFile = await buildHost.LoadProjectAsync(projectFilePath, content, LanguageNames.CSharp, CancellationToken.None); @@ -436,31 +437,57 @@ public async Task TestOpenSolution_NetCoreMultiTFMWithProjectReferenceToFSharp(b var projects = solution.Projects.ToArray(); - Assert.Equal(2, projects.Length); + AssertEx.SequenceEqual( + ["fsharplib", "csharplib(netstandard2.0)", "csharplib(netcoreapp2.0)"], + projects.Select(p => p.Name)); - foreach (var project in projects) - { - Assert.StartsWith("csharplib", project.Name); - Assert.Empty(project.ProjectReferences); + var fsharpLib = projects[0]; + var csharpLibStd = projects[1]; + var csharpLibApp = projects[2]; - if (build) - { - Assert.Empty(project.AllProjectReferences); - Assert.Contains(project.MetadataReferences, m => m is PortableExecutableReference pe && pe.FilePath.EndsWith("fsharplib.dll")); - } - else - { - Assert.Single(project.AllProjectReferences); - } + Assert.Empty(fsharpLib.ProjectReferences); + Assert.Empty(fsharpLib.AllProjectReferences); + Assert.Contains(fsharpLib.MetadataReferences, r => r is PortableExecutableReference per && Path.GetFileName(per.FilePath) == "FSharp.Core.dll"); + + // enables Hot Reload to read PDB: + Assert.EndsWith(Path.Combine("obj", "Debug", "netstandard2.0", "fsharplib.dll"), fsharpLib.CompilationOutputInfo.AssemblyPath); + + // enables Hot Reload to validate document checksums: + var libraryFs = fsharpLib.Documents.Single(d => Path.GetFileName(d.FilePath) == "Library.fs"); + var text = await libraryFs.GetTextAsync(); + + // F# fails to build with CodePage set: https://github.com/dotnet/fsharp/issues/14693 + // Assert.Equal(932, text.Encoding.CodePage); + + Assert.Equal(SourceHashAlgorithm.Sha1, text.ChecksumAlgorithm); + Assert.Equal(SourceHashAlgorithm.Sha1, fsharpLib.State.ChecksumAlgorithm); + + AssertEx.SequenceEqual([fsharpLib.Id], csharpLibStd.ProjectReferences.Select(r => r.ProjectId)); + AssertEx.SequenceEqual([fsharpLib.Id], csharpLibApp.ProjectReferences.Select(r => r.ProjectId)); + AssertEx.SequenceEqual([fsharpLib.Id], csharpLibStd.AllProjectReferences.Select(r => r.ProjectId)); + AssertEx.SequenceEqual([fsharpLib.Id], csharpLibApp.AllProjectReferences.Select(r => r.ProjectId)); + + // validate we can create C# compilation: + var compilation = await csharpLibStd.GetCompilationAsync(); + + // The reference is available even if the project is not built and the DLL doesn't exist on disk: + var reference = compilation.References.Single(r => r is PortableExecutableReference per && Path.GetFileName(per.FilePath) == "fsharplib.dll"); + + if (build) + { + Assert.NotNull(reference.GetManifestModuleMetadata()); + } + else + { + Assert.Throws(reference.GetManifestModuleMetadata); } } - [ConditionalTheory(typeof(DotNetSdkMSBuildInstalled), AlwaysSkip = "https://github.com/dotnet/roslyn/issues/81589")] + [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))] [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)] [Trait(Traits.Feature, Traits.Features.NetCore)] [WorkItem("https://github.com/dotnet/roslyn/issues/81589")] - [CombinatorialData] - public async Task TestOpenSolution_NetCoreMultiTFMWithProjectReferenceToFSharp_MultiTFM(bool build) + public async Task TestOpenSolution_NetCoreMultiTFMWithProjectReferenceToFSharp_MultiTFM() { CreateFiles(GetNetCoreMultiTFMFiles_ProjectReferenceToFSharp()); @@ -472,33 +499,35 @@ public async Task TestOpenSolution_NetCoreMultiTFMWithProjectReferenceToFSharp_M DotNetRestore("Solution.sln"); - if (build) - { - DotNetBuild("Solution.sln", configuration: "Debug"); - } - using var workspace = CreateMSBuildWorkspace(throwOnWorkspaceFailed: false, skipUnrecognizedProjects: true); var solution = await workspace.OpenSolutionAsync(solutionFilePath); - var projects = solution.Projects.ToArray(); - Assert.Equal(2, projects.Length); + AssertEx.SequenceEqual( + ["fsharplib(netstandard2.0)", "fsharplib(netcoreapp2.0)", "csharplib(netstandard2.0)", "csharplib(netcoreapp2.0)"], + projects.Select(p => p.Name)); - foreach (var project in projects) + var fsharpLibStd = projects[0]; + var fsharpLibApp = projects[1]; + var csharpLibStd = projects[2]; + var csharpLibApp = projects[3]; + + foreach (var fsharpProj in new[] { fsharpLibApp, fsharpLibStd }) { - Assert.StartsWith("csharplib", project.Name); - Assert.Empty(project.ProjectReferences); + Assert.Empty(fsharpProj.ProjectReferences); + Assert.Empty(fsharpProj.AllProjectReferences); + Assert.Contains(fsharpProj.MetadataReferences, r => r is PortableExecutableReference per && Path.GetFileName(per.FilePath) == "FSharp.Core.dll"); + Assert.Contains(fsharpProj.Documents, d => Path.GetFileName(d.FilePath) == "Library.fs"); + } - if (build) - { - Assert.Empty(project.AllProjectReferences); - Assert.Contains(project.MetadataReferences, m => m is PortableExecutableReference pe && pe.FilePath.EndsWith("fsharplib.dll")); - } - else - { - Assert.Single(project.AllProjectReferences); - } + foreach (var csharpProj in new[] { csharpLibApp, csharpLibStd }) + { + Assert.Contains(csharpProj.MetadataReferences, r => r is PortableExecutableReference per && Path.GetFileName(per.FilePath) == "FSharp.Core.dll"); + Assert.Contains(csharpProj.Documents, d => Path.GetFileName(d.FilePath) == "Class1.cs"); } + + AssertEx.SequenceEqual([fsharpLibApp.Id], csharpLibApp.ProjectReferences.Select(r => r.ProjectId)); + AssertEx.SequenceEqual([fsharpLibStd.Id], csharpLibStd.AllProjectReferences.Select(r => r.ProjectId)); } [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))] diff --git a/src/Workspaces/MSBuild/Test/NewlyCreatedProjectsFromDotNetNew.cs b/src/Workspaces/MSBuild/Test/NewlyCreatedProjectsFromDotNetNew.cs index 8bdce1879e34..52a8844a590a 100644 --- a/src/Workspaces/MSBuild/Test/NewlyCreatedProjectsFromDotNetNew.cs +++ b/src/Workspaces/MSBuild/Test/NewlyCreatedProjectsFromDotNetNew.cs @@ -147,7 +147,8 @@ private async Task AssertTemplateProjectLoadsCleanlyAsync(string templateName, s { if (ignoredDiagnostics?.Length > 0) { - TestOutput.WriteLine($""" + var logger = LoggerFactory.CreateLogger(nameof(AssertTemplateProjectLoadsCleanlyAsync)); + logger.LogInformation($""" Ignoring compiler diagnostics: "{string.Join("\", \"", ignoredDiagnostics)}" """); } diff --git a/src/Workspaces/MSBuild/Test/Resources/NetCoreMultiTFM_ProjectReferenceToFSharp/fsharplib/Library.fs b/src/Workspaces/MSBuild/Test/Resources/NetCoreMultiTFM_ProjectReferenceToFSharp/fsharplib/Library.fs index 0b741d56de26..119fae87645b 100644 --- a/src/Workspaces/MSBuild/Test/Resources/NetCoreMultiTFM_ProjectReferenceToFSharp/fsharplib/Library.fs +++ b/src/Workspaces/MSBuild/Test/Resources/NetCoreMultiTFM_ProjectReferenceToFSharp/fsharplib/Library.fs @@ -2,4 +2,4 @@ namespace fsharplib module Say = let hello name = - printfn "Hello %s" name + printfn "ɂ %s" name diff --git a/src/Workspaces/MSBuild/Test/Resources/NetCoreMultiTFM_ProjectReferenceToFSharp/fsharplib/fsharplib.fsproj b/src/Workspaces/MSBuild/Test/Resources/NetCoreMultiTFM_ProjectReferenceToFSharp/fsharplib/fsharplib.fsproj index df4543225fb4..5edcde532668 100644 --- a/src/Workspaces/MSBuild/Test/Resources/NetCoreMultiTFM_ProjectReferenceToFSharp/fsharplib/fsharplib.fsproj +++ b/src/Workspaces/MSBuild/Test/Resources/NetCoreMultiTFM_ProjectReferenceToFSharp/fsharplib/fsharplib.fsproj @@ -2,6 +2,11 @@ netstandard2.0 + + sha1 diff --git a/src/Workspaces/MSBuild/Test/Utilities/DotNetSdkMSBuildInstalled.cs b/src/Workspaces/MSBuild/Test/Utilities/DotNetSdkMSBuildInstalled.cs index 6fc7abce2bf5..eec6af4bdce8 100644 --- a/src/Workspaces/MSBuild/Test/Utilities/DotNetSdkMSBuildInstalled.cs +++ b/src/Workspaces/MSBuild/Test/Utilities/DotNetSdkMSBuildInstalled.cs @@ -40,7 +40,7 @@ private static bool HasNetCoreSdkForSolution(string solution) try { - buildHostProcessManager = new BuildHostProcessManager(); + buildHostProcessManager = new BuildHostProcessManager(knownCommandLineParserLanguages: []); var buildHost = buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None).Result; diff --git a/src/Workspaces/MSBuild/Test/Utilities/VisualStudioMSBuildInstalled.cs b/src/Workspaces/MSBuild/Test/Utilities/VisualStudioMSBuildInstalled.cs index 347ce870f7f8..e11d6eca91ac 100644 --- a/src/Workspaces/MSBuild/Test/Utilities/VisualStudioMSBuildInstalled.cs +++ b/src/Workspaces/MSBuild/Test/Utilities/VisualStudioMSBuildInstalled.cs @@ -30,7 +30,7 @@ private static bool IsVisualStudioMSBuildInstalled() try { - buildHostProcessManager = new BuildHostProcessManager(); + buildHostProcessManager = new BuildHostProcessManager(knownCommandLineParserLanguages: []); var buildHost = buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetFramework, CancellationToken.None).Result; diff --git a/src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs b/src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs index 5854b1ed030d..96da4f1cc184 100644 --- a/src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs +++ b/src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs @@ -875,16 +875,31 @@ public async Task TestOpenProject_ProjectFileExtensionAssociatedWithUnknownLangu CreateFiles(GetSimpleCSharpSolutionFiles()); var projFileName = GetSolutionFileName(@"CSharpProject\CSharpProject.csproj"); var language = "lingo"; - var e = await Assert.ThrowsAsync(async delegate - { - var ws = MSBuildWorkspace.Create(); - ws.AssociateFileExtensionWithLanguage("csproj", language); // non-existent language - await ws.OpenProjectAsync(projFileName); - }); + var ws = MSBuildWorkspace.Create(); - // the exception should tell us something about the language being unrecognized. - var expected = string.Format(WorkspacesResources.Cannot_open_project_0_because_the_language_1_is_not_supported, projFileName, language); - Assert.Equal(expected, e.Message); + ws.AssociateFileExtensionWithLanguage("csproj", language); // non-existent language + var project = await ws.OpenProjectAsync(projFileName); + Assert.Equal(language, project.Language); + + // parse and compilation options should be null since language is unknown + Assert.Null(project.ParseOptions); + Assert.Null(project.CompilationOptions); + + // documents and metadata references should still be loaded: + AssertEx.SequenceEqual( + [ + "CSharpClass.cs", + "AssemblyInfo.cs", + ".NETFramework,Version=v4.8.AssemblyAttributes.cs" + ], project.Documents.Select(d => d.Name)); + + AssertEx.Equal( + [ + "Microsoft.CSharp.dll", + "mscorlib.dll", + "System.Core.dll", + "System.dll", + ], project.MetadataReferences.Select(r => Path.GetFileName(((PortableExecutableReference)r).FilePath))); } [ConditionalFact(typeof(VisualStudioMSBuildInstalled))] @@ -1142,55 +1157,36 @@ public async Task TestOpenSolution_WithUnrecognizedProjectTypeGuidAndUnrecognize [ConditionalFact(typeof(VisualStudioMSBuildInstalled))] [WorkItem("https://github.com/dotnet/roslyn/issues/3931")] - public async Task TestOpenSolution_WithMissingLanguageLibraries_WithSkipFalse_ThrowsAsync() - { - // proves that if the language libraries are missing then the appropriate error occurs - CreateFiles(GetSimpleCSharpSolutionFiles()); - var solutionFilePath = GetSolutionFileName(@"TestSolution.sln"); - - var e = await Assert.ThrowsAsync(async () => - { - using var workspace = CreateMSBuildWorkspace(MefHostServices.Create(_defaultAssembliesWithoutCSharp)); - workspace.SkipUnrecognizedProjects = false; - await workspace.OpenSolutionAsync(solutionFilePath); - }); - - var projFileName = GetSolutionFileName(@"CSharpProject\CSharpProject.csproj"); - var expected = string.Format(WorkspacesResources.Cannot_open_project_0_because_the_language_1_is_not_supported, projFileName, LanguageNames.CSharp); - Assert.Equal(expected, e.Message); - } - - [ConditionalFact(typeof(VisualStudioMSBuildInstalled))] - [WorkItem("https://github.com/dotnet/roslyn/issues/3931")] - public async Task TestOpenSolution_WithMissingLanguageLibraries_WithSkipTrue_SucceedsWithDiagnostic() + public async Task TestOpenProject_WithMissingLanguageLibraries() { // proves that if the language libraries are missing then the appropriate error occurs CreateFiles(GetSimpleCSharpSolutionFiles()); - var solutionFilePath = GetSolutionFileName(@"TestSolution.sln"); + var projectName = GetSolutionFileName(@"CSharpProject\CSharpProject.csproj"); - using var workspace = CreateMSBuildWorkspace(MefHostServices.Create(_defaultAssembliesWithoutCSharp)); - workspace.SkipUnrecognizedProjects = true; + using var workspace = MSBuildWorkspace.Create(MefHostServices.Create(_defaultAssembliesWithoutCSharp)); + var project = await workspace.OpenProjectAsync(projectName); - var solution = await workspace.OpenSolutionAsync(solutionFilePath); + Assert.Equal(LanguageNames.CSharp, project.Language); - var projFileName = GetSolutionFileName(@"CSharpProject\CSharpProject.csproj"); - var expected = string.Format(WorkspacesResources.Cannot_open_project_0_because_the_language_1_is_not_supported, projFileName, LanguageNames.CSharp); - Assert.Equal(expected, workspace.Diagnostics.Single().Message); - } + // parse and compilation options should be null since language is unknown + Assert.Null(project.ParseOptions); + Assert.Null(project.CompilationOptions); - [ConditionalFact(typeof(VisualStudioMSBuildInstalled))] - [WorkItem("https://github.com/dotnet/roslyn/issues/3931")] - public async Task TestOpenProject_WithMissingLanguageLibraries_Throws() - { - // proves that if the language libraries are missing then the appropriate error occurs - CreateFiles(GetSimpleCSharpSolutionFiles()); - var projectName = GetSolutionFileName(@"CSharpProject\CSharpProject.csproj"); - - using var workspace = MSBuildWorkspace.Create(MefHostServices.Create(_defaultAssembliesWithoutCSharp)); - var e = await Assert.ThrowsAsync(() => workspace.OpenProjectAsync(projectName)); + // documents and metadata references should still be loaded: + AssertEx.SequenceEqual( + [ + "CSharpClass.cs", + "AssemblyInfo.cs", + ".NETFramework,Version=v4.8.AssemblyAttributes.cs" + ], project.Documents.Select(d => d.Name)); - var expected = string.Format(WorkspacesResources.Cannot_open_project_0_because_the_language_1_is_not_supported, projectName, LanguageNames.CSharp); - Assert.Equal(expected, e.Message); + AssertEx.Equal( + [ + "Microsoft.CSharp.dll", + "mscorlib.dll", + "System.Core.dll", + "System.dll", + ], project.MetadataReferences.Select(r => Path.GetFileName(((PortableExecutableReference)r).FilePath))); } [ConditionalFact(typeof(VisualStudioMSBuildInstalled))] @@ -3100,7 +3096,7 @@ public async Task TestOpenProject_CommandLineArgsHaveNoErrors() var projectFilePath = GetSolutionFileName(@"CSharpProject\CSharpProject.csproj"); - await using var buildHostProcessManager = new BuildHostProcessManager(ImmutableDictionary.Empty); + await using var buildHostProcessManager = new BuildHostProcessManager(knownCommandLineParserLanguages: [LanguageNames.CSharp], ImmutableDictionary.Empty); var buildHost = await buildHostProcessManager.GetBuildHostWithFallbackAsync(projectFilePath, CancellationToken.None); var projectFile = await buildHost.LoadProjectFileAsync(projectFilePath, LanguageNames.CSharp, CancellationToken.None); diff --git a/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs b/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs index 03ff0171862a..9c739aa25879 100644 --- a/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs +++ b/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs @@ -70,11 +70,8 @@ protected override JsonRpcConnection CreateConnection(JsonRpc jsonRpc) internal const string DebuggerComponentName = "Debugger"; public static readonly ServiceRpcDescriptor SolutionSnapshotProvider = CreateClientServiceDescriptor("SolutionSnapshotProvider", new Version(0, 1)); - public static readonly ServiceRpcDescriptor DebuggerManagedHotReloadService = CreateDebuggerServiceDescriptor("ManagedHotReloadService", new Version(0, 1)); - public static readonly ServiceRpcDescriptor HotReloadLoggerService = CreateDebuggerServiceDescriptor("HotReloadLogger", new Version(0, 1)); - public static readonly ServiceRpcDescriptor HotReloadSessionNotificationService = CreateDebuggerServiceDescriptor("HotReloadSessionNotificationService", new Version(0, 1)); - public static readonly ServiceRpcDescriptor ManagedHotReloadAgentManagerService = CreateDebuggerServiceDescriptor("ManagedHotReloadAgentManagerService", new Version(0, 1)); - public static readonly ServiceRpcDescriptor GenericHotReloadAgentManagerService = CreateDebuggerServiceDescriptor("GenericHotReloadAgentManagerService", new Version(0, 1)); + public static readonly ServiceRpcDescriptor DebuggerManagedHotReloadService = CreateDebuggerServiceDescriptor("ManagedHotReloadService", new Version(1, 0)); + public static readonly ServiceRpcDescriptor HotReloadLoggerService = CreateDebuggerServiceDescriptor("HotReloadLogger", new Version(1, 0)); public static readonly ServiceRpcDescriptor HotReloadOptionService = CreateDebuggerClientServiceDescriptor("HotReloadOptionService", new Version(0, 1)); public static readonly ServiceRpcDescriptor MauiLaunchCustomizerService = CreateMauiServiceDescriptor("MauiLaunchCustomizerService", new Version(0, 1)); public static readonly ServiceRpcDescriptor CssVisualDiagnosticsService = CreateWebToolsServiceDescriptor("CssVisualDiagnosticsService", new Version(0, 1)); diff --git a/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs b/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs deleted file mode 100644 index 2e0c5db7db8a..000000000000 --- a/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs +++ /dev/null @@ -1,290 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.ComponentModel.Composition; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.BrokeredServices; -using Microsoft.CodeAnalysis.Contracts.EditAndContinue; -using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.EditAndContinue; - -[Export(typeof(IManagedHotReloadLanguageService))] -[Export(typeof(IManagedHotReloadLanguageService2))] -[Export(typeof(IManagedHotReloadLanguageService3))] -[method: ImportingConstructor] -[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed partial class ManagedHotReloadLanguageService( - IServiceBrokerProvider serviceBrokerProvider, - IEditAndContinueService encService, - SolutionSnapshotRegistry solutionSnapshotRegistry) : IManagedHotReloadLanguageService3 -{ - private sealed class PdbMatchingSourceTextProvider : IPdbMatchingSourceTextProvider - { - public static readonly PdbMatchingSourceTextProvider Instance = new(); - - // Returning null will check the file on disk: - public async ValueTask TryGetMatchingSourceTextAsync(string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) - => null; - } - - private static readonly ActiveStatementSpanProvider s_emptyActiveStatementProvider = - async (_, _, _) => ImmutableArray.Empty; - - private readonly ManagedHotReloadServiceProxy _debuggerService = new(serviceBrokerProvider.ServiceBroker); - private readonly SolutionSnapshotProviderProxy _solutionSnapshotProvider = new(serviceBrokerProvider.ServiceBroker); - - private bool _disabled; - private DebuggingSessionId? _debuggingSession; - private Solution? _committedSolution; - private Solution? _pendingUpdatedSolution; - - private void Disable() - { - _disabled = true; - _debuggingSession = null; - _committedSolution = null; - _pendingUpdatedSolution = null; - solutionSnapshotRegistry.Clear(); - } - - private async ValueTask GetCurrentSolutionAsync(CancellationToken cancellationToken) - { - // First, calls to the client to get the current snapshot id. - // The client service calls the LSP client, which sends message to the LSP server, which in turn calls back to RegisterSolutionSnapshot. - // Once complete the snapshot should be registered. - var id = await _solutionSnapshotProvider.RegisterSolutionSnapshotAsync(cancellationToken).ConfigureAwait(false); - - return solutionSnapshotRegistry.GetRegisteredSolutionSnapshot(id); - } - - public async ValueTask StartSessionAsync(CancellationToken cancellationToken) - { - if (_disabled) - { - return; - } - - try - { - var currentSolution = await GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false); - _committedSolution = currentSolution; - - // TODO: use remote proxy once we transition to pull diagnostics - _debuggingSession = encService.StartDebuggingSession( - currentSolution, - _debuggerService, - PdbMatchingSourceTextProvider.Instance, - reportDiagnostics: true); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - // the service failed, error has been reported - disable further operations - Disable(); - } - } - - private async ValueTask BreakStateOrCapabilitiesChangedAsync(bool? inBreakState, CancellationToken cancellationToken) - { - if (_disabled) - { - return; - } - - try - { - Contract.ThrowIfNull(_debuggingSession); - encService.BreakStateOrCapabilitiesChanged(_debuggingSession.Value, inBreakState); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - Disable(); - } - } - - public ValueTask EnterBreakStateAsync(CancellationToken cancellationToken) - => BreakStateOrCapabilitiesChangedAsync(inBreakState: true, cancellationToken); - - public ValueTask ExitBreakStateAsync(CancellationToken cancellationToken) - => BreakStateOrCapabilitiesChangedAsync(inBreakState: false, cancellationToken); - - public ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellationToken) - => BreakStateOrCapabilitiesChangedAsync(inBreakState: null, cancellationToken); - - public async ValueTask CommitUpdatesAsync(CancellationToken cancellationToken) - { - if (_disabled) - { - return; - } - - try - { - Contract.ThrowIfNull(_debuggingSession); - var committedSolution = Interlocked.Exchange(ref _pendingUpdatedSolution, null); - Contract.ThrowIfNull(committedSolution); - - _committedSolution = committedSolution; - - encService.CommitSolutionUpdate(_debuggingSession.Value); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - Disable(); - } - } - - [Obsolete] - public ValueTask UpdateBaselinesAsync(ImmutableArray projectPaths, CancellationToken cancellationToken) - => throw new NotImplementedException(); - - public async ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken) - { - if (_disabled) - { - return; - } - - try - { - Contract.ThrowIfNull(_debuggingSession); - Contract.ThrowIfNull(Interlocked.Exchange(ref _pendingUpdatedSolution, null)); - - encService.DiscardSolutionUpdate(_debuggingSession.Value); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - Disable(); - } - } - - public async ValueTask EndSessionAsync(CancellationToken cancellationToken) - { - if (_disabled) - { - return; - } - - try - { - Contract.ThrowIfNull(_debuggingSession); - - encService.EndDebuggingSession(_debuggingSession.Value); - - _debuggingSession = null; - _committedSolution = null; - _pendingUpdatedSolution = null; - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - Disable(); - } - } - - /// - /// Returns true if any changes have been made to the source since the last changes had been applied. - /// For performance reasons it only implements a heuristic and may return both false positives and false negatives. - /// If the result is a false negative the debugger will not apply the changes unless the user explicitly triggers apply change command. - /// The background diagnostic analysis will still report rude edits for these ignored changes. It may also happen that these rude edits - /// will disappear once the debuggee is resumed - if they are caused by presence of active statements around the change. - /// If the result is a false positive the debugger attempts to apply the changes, which will result in a delay but will correctly end up - /// with no actual deltas to be applied. - /// - /// If is specified checks for changes only in a document of the given path. - /// This is not supported (returns false) for source-generated documents. - /// - public async ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) - { - try - { - var debuggingSession = _debuggingSession; - if (debuggingSession == null) - { - return false; - } - - Contract.ThrowIfNull(_committedSolution); - var oldSolution = _committedSolution; - - var newSolution = await GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false); - - return (sourceFilePath != null) - ? await EditSession.HasChangesAsync(oldSolution, newSolution, sourceFilePath, cancellationToken).ConfigureAwait(false) - : await EditSession.HasChangesAsync(oldSolution, newSolution, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - return true; - } - } - - [Obsolete] - public ValueTask GetUpdatesAsync(CancellationToken cancellationToken) - => throw new NotImplementedException(); - - [Obsolete] - public ValueTask GetUpdatesAsync(ImmutableArray runningProjects, CancellationToken cancellationToken) - => throw new NotImplementedException(); - - public async ValueTask GetUpdatesAsync(ImmutableArray runningProjects, CancellationToken cancellationToken) - { - if (_disabled) - { - return new ManagedHotReloadUpdates([], [], [], []); - } - - try - { - Contract.ThrowIfNull(_debuggingSession); - - var solution = await GetCurrentSolutionAsync(cancellationToken).ConfigureAwait(false); - var runningProjectOptions = runningProjects.ToRunningProjectOptions(solution, static info => (info.ProjectInstanceId.ProjectFilePath, info.ProjectInstanceId.TargetFramework, info.RestartAutomatically)); - - EmitSolutionUpdateResults.Data results; - - try - { - results = (await encService.EmitSolutionUpdateAsync(_debuggingSession.Value, solution, runningProjectOptions, s_emptyActiveStatementProvider, cancellationToken).ConfigureAwait(false)).Dehydrate(); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - results = EmitSolutionUpdateResults.Data.CreateFromInternalError(solution, e.Message, runningProjectOptions); - } - - // Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called. - if (results.ModuleUpdates.Status == ModuleUpdateStatus.Ready) - { - _pendingUpdatedSolution = solution; - } - - return new ManagedHotReloadUpdates( - results.ModuleUpdates.Updates, - results.GetAllDiagnostics(), - ToProjectIntanceIds(results.ProjectsToRebuild), - ToProjectIntanceIds(results.ProjectsToRestart.Keys)); - - ImmutableArray ToProjectIntanceIds(IEnumerable ids) - => ids.SelectAsArray(id => - { - var project = solution.GetRequiredProject(id); - return new ProjectInstanceId(project.FilePath!, project.State.NameAndFlavor.flavor ?? ""); - }); - } - catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) - { - Disable(); - return new ManagedHotReloadUpdates([], [], [], []); - } - } -} diff --git a/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadServiceProxy.cs b/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadServiceProxy.cs deleted file mode 100644 index f3044652d540..000000000000 --- a/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadServiceProxy.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.BrokeredServices; -using Microsoft.CodeAnalysis.Contracts.EditAndContinue; -using Microsoft.ServiceHub.Framework; - -namespace Microsoft.CodeAnalysis.EditAndContinue; - -internal sealed class ManagedHotReloadServiceProxy(IServiceBroker serviceBroker) : - BrokeredServiceProxy(serviceBroker, BrokeredServiceDescriptors.DebuggerManagedHotReloadService), - IManagedHotReloadService -{ - public ValueTask> GetActiveStatementsAsync(CancellationToken cancellationToken) - => InvokeAsync((service, cancellationToken) => service.GetActiveStatementsAsync(cancellationToken), cancellationToken); - - public ValueTask GetAvailabilityAsync(Guid module, CancellationToken cancellationToken) - => InvokeAsync((service, module, cancellationToken) => service.GetAvailabilityAsync(module, cancellationToken), module, cancellationToken); - - public ValueTask> GetCapabilitiesAsync(CancellationToken cancellationToken) - => InvokeAsync((service, cancellationToken) => service.GetCapabilitiesAsync(cancellationToken), cancellationToken); - - public ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellationToken) - => InvokeAsync((service, module, cancellationToken) => service.PrepareModuleForUpdateAsync(module, cancellationToken), module, cancellationToken); -} diff --git a/src/Workspaces/Remote/Core/EditAndContinue/SolutionSnapshotProviderProxy.cs b/src/Workspaces/Remote/Core/EditAndContinue/SolutionSnapshotProviderProxy.cs deleted file mode 100644 index 9f5119f0b167..000000000000 --- a/src/Workspaces/Remote/Core/EditAndContinue/SolutionSnapshotProviderProxy.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.BrokeredServices; -using Microsoft.CodeAnalysis.Contracts.Client; -using Microsoft.ServiceHub.Framework; - -namespace Microsoft.CodeAnalysis.EditAndContinue; - -internal sealed class SolutionSnapshotProviderProxy(IServiceBroker serviceBroker) : - BrokeredServiceProxy(serviceBroker, BrokeredServiceDescriptors.SolutionSnapshotProvider), - ISolutionSnapshotProvider -{ - public ValueTask RegisterSolutionSnapshotAsync(CancellationToken cancellationToken) - => InvokeAsync((service, cancellationToken) => service.RegisterSolutionSnapshotAsync(cancellationToken), cancellationToken); -} diff --git a/src/Workspaces/Remote/Core/RemoteCallback.cs b/src/Workspaces/Remote/Core/RemoteCallback.cs index 1e055eabd669..7d42a031ad0b 100644 --- a/src/Workspaces/Remote/Core/RemoteCallback.cs +++ b/src/Workspaces/Remote/Core/RemoteCallback.cs @@ -111,12 +111,18 @@ public async ValueTask InvokeAsync( var writeTask = WriteAsync(_callback, pipe.Writer); var readTask = ReadAsync(pipe.Reader); - // Note: waiting on the write-task is not strictly necessary. The read-task cannot complete unless it - // the write-task completes (or it faults for some reason). However, it's nice and clean to just not - // use fire-and-forget here and avoids us having to consider things like async-tracking-tokens for - // testing purposes. - await Task.WhenAll(writeTask, readTask).ConfigureAwait(false); - await readTask.ConfigureAwait(false); + // Wait for both tasks to be complete, ensuring that if either failed we will rethrow the failure. Previously, this code + // used just Task.WhenAll() to simultaneously wait for both tasks to complete, but this could result in deadlocks. If the + // readTask faulted for some reason, the pipe would start to fill up as writeTask continued to run. At some point the pipe + // could hit it's configured maximum size and then writeTask would block waiting for the read to catch up. Theoretically, + // a failure of the writeTask should still end the pipe, so the readTask would eventually complete, but we'll be sufficiently + // paranoid here to ensure that no matter what happens we won't end up in a deadlock. + var firstTask = await Task.WhenAny(readTask, writeTask).ConfigureAwait(false); + + // Rethrow the exception if firstTask faulted + await firstTask.ConfigureAwait(false); + + await Task.WhenAll(readTask, writeTask).ConfigureAwait(false); } catch (Exception exception) when (ReportUnexpectedException(exception, cancellationToken)) { diff --git a/src/Workspaces/Remote/ServiceHub.CoreComponents/arm64/Microsoft.CodeAnalysis.Remote.ServiceHub.CoreComponents.arm64.csproj b/src/Workspaces/Remote/ServiceHub.CoreComponents/arm64/Microsoft.CodeAnalysis.Remote.ServiceHub.CoreComponents.arm64.csproj index 6f87afcb2454..c2fd207dcf40 100644 --- a/src/Workspaces/Remote/ServiceHub.CoreComponents/arm64/Microsoft.CodeAnalysis.Remote.ServiceHub.CoreComponents.arm64.csproj +++ b/src/Workspaces/Remote/ServiceHub.CoreComponents/arm64/Microsoft.CodeAnalysis.Remote.ServiceHub.CoreComponents.arm64.csproj @@ -5,8 +5,6 @@ Exe $(NetVS) arm64 - - $(NoWarn);NETSDK1206 diff --git a/src/Workspaces/Remote/ServiceHub.CoreComponents/x64/Microsoft.CodeAnalysis.Remote.ServiceHub.CoreComponents.x64.csproj b/src/Workspaces/Remote/ServiceHub.CoreComponents/x64/Microsoft.CodeAnalysis.Remote.ServiceHub.CoreComponents.x64.csproj index da1891a99ac5..31c5df03c871 100644 --- a/src/Workspaces/Remote/ServiceHub.CoreComponents/x64/Microsoft.CodeAnalysis.Remote.ServiceHub.CoreComponents.x64.csproj +++ b/src/Workspaces/Remote/ServiceHub.CoreComponents/x64/Microsoft.CodeAnalysis.Remote.ServiceHub.CoreComponents.x64.csproj @@ -6,8 +6,6 @@ $(NetVS) x64 - - $(NoWarn);NETSDK1206 diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs index 2fd6afe32773..6b09927446f0 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -208,26 +209,68 @@ private async Task GetOrCreateSolutionToUpdateAsync( CancellationToken cancellationToken) { // See if we can just incrementally update the current solution. - var currentSolution = this.CurrentSolution; - if (await IsIncrementalUpdateAsync().ConfigureAwait(false)) - return currentSolution; + var newSolutionCompilationChecksums = await assetProvider.GetAssetAsync( + AssetPathKind.SolutionCompilationStateChecksums, solutionChecksum, cancellationToken).ConfigureAwait(false); + var newSolutionChecksums = await assetProvider.GetAssetAsync( + AssetPathKind.SolutionStateChecksums, newSolutionCompilationChecksums.SolutionState, cancellationToken).ConfigureAwait(false); + var newSolutionInfo = await assetProvider.GetAssetAsync( + AssetPathKind.SolutionAttributes, newSolutionChecksums.Attributes, cancellationToken).ConfigureAwait(false); + + // We update CurrentSolution by periodically syncing the primary solution over from our main process. During solution load, that might not happen prior to + // a file being opened and the user trying to interact with a project; in that case CurrentSolution might still be empty. In that window of time, + // each operation from OOP would then synchronize over the projects or cone from scratch, and then effectively throwing away that state since the next request will + // try to base itself from the (empty) CurrentSolution again. To avoid that, we'll see if we have some other forked solution available that has some of the projects we might + // need, but checking how many projects are missing. This is especially important for cases where the projects may have generators, since a first time generator run + // could be expensive. + Solution? bestSolution = null; + int? bestSolutionProjectsMissingCount = null; + + // First, is our CurrentSolution already a candidate that can't be beat? + UpdateBestSolution(this.CurrentSolution); + if (bestSolution is not null && bestSolutionProjectsMissingCount == 0) + return bestSolution; + + using var _ = PooledHashSet.GetInstance(out var solutions); + using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) + { + _lastRequestedAnyBranchSolutions.AddAllTo(solutions); + } + + foreach (var solution in solutions) + UpdateBestSolution(solution); + + if (bestSolution is not null) + return bestSolution; // If not, have to create a new, fresh, solution instance to update. var solutionInfo = await assetProvider.CreateSolutionInfoAsync( solutionChecksum, this.Services.SolutionServices, cancellationToken).ConfigureAwait(false); return CreateSolutionFromInfo(solutionInfo); - async Task IsIncrementalUpdateAsync() + // Updates , preferring whichever Solution will require synchronizing over the fewest new projects. + void UpdateBestSolution(Solution candidateSolution) { - var newSolutionCompilationChecksums = await assetProvider.GetAssetAsync( - AssetPathKind.SolutionCompilationStateChecksums, solutionChecksum, cancellationToken).ConfigureAwait(false); - var newSolutionChecksums = await assetProvider.GetAssetAsync( - AssetPathKind.SolutionStateChecksums, newSolutionCompilationChecksums.SolutionState, cancellationToken).ConfigureAwait(false); - var newSolutionInfo = await assetProvider.GetAssetAsync( - AssetPathKind.SolutionAttributes, newSolutionChecksums.Attributes, cancellationToken).ConfigureAwait(false); - - // if either solution id or file path changed, then we consider it as new solution - return currentSolution.Id == newSolutionInfo.Id && currentSolution.FilePath == newSolutionInfo.FilePath; + if (candidateSolution.Id == newSolutionInfo.Id && candidateSolution.FilePath == newSolutionInfo.FilePath) + { + // Compute how many projects are missing from this one + using var _ = PooledHashSet.GetInstance(out var ids); + ids.AddAll(newSolutionChecksums.Projects.Ids); + foreach (var id in candidateSolution.ProjectIds) + ids.Remove(id); + + var projectsMissing = ids.Count; + + if (!bestSolutionProjectsMissingCount.HasValue) + { + bestSolution = candidateSolution; + bestSolutionProjectsMissingCount = projectsMissing; + } + else if (projectsMissing < bestSolutionProjectsMissingCount.Value) + { + bestSolution = candidateSolution; + bestSolutionProjectsMissingCount = projectsMissing; + } + } } } diff --git a/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs b/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs index 6ee5157971eb..1c85ef67406c 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs @@ -27,7 +27,7 @@ static SolutionAssetCache() } /// - /// Workspace we are associated with. When we purge items from teh cache, we will avoid any items associated + /// Workspace we are associated with. When we purge items from the cache, we will avoid any items associated /// with the items in its 'CurrentSolution'. /// private readonly RemoteWorkspace? _remoteWorkspace; diff --git a/src/Workspaces/Remote/ServiceHub/Services/Copilot/RemoteCopilotProposalAdjusterService.cs b/src/Workspaces/Remote/ServiceHub/Services/Copilot/RemoteCopilotProposalAdjusterService.cs index 6cb96d5f5962..ceae22e747c1 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/Copilot/RemoteCopilotProposalAdjusterService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/Copilot/RemoteCopilotProposalAdjusterService.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Copilot; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; @@ -23,7 +24,8 @@ protected override IRemoteCopilotProposalAdjusterService CreateService(in Servic public ValueTask TryAdjustProposalAsync( ImmutableHashSet allowableAdjustments, - Checksum solutionChecksum, DocumentId documentId, ImmutableArray textChanges, CancellationToken cancellationToken) + Checksum solutionChecksum, DocumentId documentId, ImmutableArray textChanges, + LineFormattingOptions? lineFormattingOptions, CancellationToken cancellationToken) { return RunServiceAsync(solutionChecksum, async solution => { @@ -31,7 +33,7 @@ public ValueTask TryAdjustProposalAsync( documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); var service = document.GetRequiredLanguageService(); - return await service.TryAdjustProposalAsync(allowableAdjustments, document, textChanges, cancellationToken).ConfigureAwait(false); + return await service.TryAdjustProposalAsync(allowableAdjustments, document, textChanges, lineFormattingOptions, cancellationToken).ConfigureAwait(false); }, cancellationToken); } } diff --git a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs index c36ad6e04075..140c8f8b96f5 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/DiagnosticAnalyzer/RemoteDiagnosticAnalyzerService.cs @@ -193,6 +193,23 @@ public ValueTask> GetDiagnosticDescript cancellationToken); } + public ValueTask>> GetAllDiagnosticIdsAsync( + Checksum solutionChecksum, + ImmutableArray projectIds, + CancellationToken cancellationToken) + { + return RunWithSolutionAsync( + solutionChecksum, + async solution => + { + var service = solution.Services.GetRequiredService(); + var list = await service.GetAllDiagnosticIdsAsync(solution, projectIds, cancellationToken).ConfigureAwait(false); + return list; + + }, + cancellationToken); + } + public ValueTask>> GetDiagnosticDescriptorsPerReferenceAsync( Checksum solutionChecksum, ProjectId? projectId, diff --git a/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/EditAndContinueLogReporter.cs b/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/EditAndContinueLogReporter.cs index ddb7f483655d..e7c079785c82 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/EditAndContinueLogReporter.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/EditAndContinueLogReporter.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#pragma warning disable CS0436 // Type conflicts with imported type (workaround for https://github.com/dotnet/roslyn/issues/76674) - using System; using System.Composition; using System.Threading; diff --git a/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/RemoteEditAndContinueService.cs b/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/RemoteEditAndContinueService.cs index 0900f79dc119..c48569800e1b 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/RemoteEditAndContinueService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/EditAndContinue/RemoteEditAndContinueService.cs @@ -151,7 +151,7 @@ public ValueTask> GetDocumentDiagnosticsAsync(Che } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { - return EmitSolutionUpdateResults.Data.CreateFromInternalError(solution, e.Message, runningProjects); + return EmitSolutionUpdateResults.Data.CreateFromInternalError(solution, e.ToString(), runningProjects); } }, cancellationToken); } diff --git a/src/Workspaces/Remote/ServiceHub/Services/LegacySolutionEvents/RemoteLegacySolutionEventsAggregationService.cs b/src/Workspaces/Remote/ServiceHub/Services/LegacySolutionEvents/RemoteLegacySolutionEventsAggregationService.cs index 7d4739b255a2..50c0ec4699e9 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/LegacySolutionEvents/RemoteLegacySolutionEventsAggregationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/LegacySolutionEvents/RemoteLegacySolutionEventsAggregationService.cs @@ -39,6 +39,7 @@ public ValueTask OnWorkspaceChangedAsync( WorkspaceChangeKind kind, ProjectId? projectId, DocumentId? documentId, + bool processSourceGeneratedDocuments, CancellationToken cancellationToken) { return RunServiceAsync(oldSolutionChecksum, newSolutionChecksum, @@ -46,7 +47,7 @@ public ValueTask OnWorkspaceChangedAsync( { var aggregationService = oldSolution.Services.GetRequiredService(); await aggregationService.OnWorkspaceChangedAsync( - new WorkspaceChangeEventArgs(kind, oldSolution, newSolution, projectId, documentId), cancellationToken).ConfigureAwait(false); + new WorkspaceChangeEventArgs(kind, oldSolution, newSolution, projectId, documentId), processSourceGeneratedDocuments, cancellationToken).ConfigureAwait(false); }, cancellationToken); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeGeneration/CSharpSyntaxTokens.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeGeneration/CSharpSyntaxTokens.cs index 34bdac702acb..b7ccb7894287 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeGeneration/CSharpSyntaxTokens.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CodeGeneration/CSharpSyntaxTokens.cs @@ -9,6 +9,7 @@ namespace Microsoft.CodeAnalysis.CSharp; internal static class CSharpSyntaxTokens { public static readonly SyntaxToken AbstractKeyword = Token(SyntaxKind.AbstractKeyword); + public static readonly SyntaxToken AsKeyword = Token(SyntaxKind.AsKeyword); public static readonly SyntaxToken AssemblyKeyword = Token(SyntaxKind.AssemblyKeyword); public static readonly SyntaxToken AsyncKeyword = Token(SyntaxKind.AsyncKeyword); public static readonly SyntaxToken AwaitKeyword = Token(SyntaxKind.AwaitKeyword); @@ -32,7 +33,7 @@ internal static class CSharpSyntaxTokens public static readonly SyntaxToken EndOfDocumentationCommentToken = Token(SyntaxKind.EndOfDocumentationCommentToken); public static readonly SyntaxToken EqualsToken = Token(SyntaxKind.EqualsToken); public static readonly SyntaxToken ExplicitKeyword = Token(SyntaxKind.ExplicitKeyword); -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN public static readonly SyntaxToken ExtensionKeyword = Token(SyntaxKind.ExtensionKeyword); #endif public static readonly SyntaxToken ExternKeyword = Token(SyntaxKind.ExternKeyword); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/LanguageVersionExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/LanguageVersionExtensions.cs index 195117c34a52..bfc7d9e20a9b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/LanguageVersionExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/LanguageVersionExtensions.cs @@ -7,9 +7,9 @@ namespace Microsoft.CodeAnalysis.CSharp.Shared.Extensions; internal static class LanguageVersionExtensions { public static bool IsCSharp15OrAbove(this LanguageVersion languageVersion) - => languageVersion >= LanguageVersion.Preview; + => languageVersion >= CSharpNext; -#if ROSLYN_4_12_OR_LOWER +#if OLDER_ROSLYN public static bool IsCSharp14OrAbove(this LanguageVersion languageVersion) => (int)languageVersion >= 1400; #else diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/MemberDeclarationSyntaxExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/MemberDeclarationSyntaxExtensions.cs index 66e1943fd071..ec30848a7f02 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/MemberDeclarationSyntaxExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Extensions/MemberDeclarationSyntaxExtensions.cs @@ -32,8 +32,9 @@ public static SyntaxToken GetNameToken(this MemberDeclarationSyntax member) case SyntaxKind.EnumDeclaration: return ((EnumDeclarationSyntax)member).Identifier; case SyntaxKind.ClassDeclaration: -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN case SyntaxKind.ExtensionBlockDeclaration: + case SyntaxKind.UnionDeclaration: #endif case SyntaxKind.InterfaceDeclaration: case SyntaxKind.RecordDeclaration: @@ -74,8 +75,9 @@ public static int GetArity(this MemberDeclarationSyntax member) switch (member.Kind()) { case SyntaxKind.ClassDeclaration: -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN case SyntaxKind.ExtensionBlockDeclaration: + case SyntaxKind.UnionDeclaration: #endif case SyntaxKind.InterfaceDeclaration: case SyntaxKind.RecordDeclaration: @@ -101,8 +103,9 @@ public static int GetArity(this MemberDeclarationSyntax member) switch (member.Kind()) { case SyntaxKind.ClassDeclaration: -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN case SyntaxKind.ExtensionBlockDeclaration: + case SyntaxKind.UnionDeclaration: #endif case SyntaxKind.InterfaceDeclaration: case SyntaxKind.RecordDeclaration: diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/NewLineUserSettingFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/NewLineUserSettingFormattingRule.cs index 3418bde62bc5..fe182e1761e1 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/NewLineUserSettingFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/NewLineUserSettingFormattingRule.cs @@ -165,7 +165,10 @@ or SyntaxKind.ImplicitArrayCreationExpression var currentTokenParentParent = currentToken.Parent.Parent; // * { - in the member declaration context - if (currentToken.IsKind(SyntaxKind.OpenBraceToken) && currentTokenParentParent is MemberDeclarationSyntax) + // In top-level code, everything is a GlobalStatementSyntax which inherits from MemberDeclarationSyntax, + // but we don't want to format a plain open brace as though it's from a member declaration. + if (currentToken.IsKind(SyntaxKind.OpenBraceToken) && + currentTokenParentParent is MemberDeclarationSyntax and not GlobalStatementSyntax) { var option = currentTokenParentParent is BasePropertyDeclarationSyntax ? _options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInProperties) @@ -366,7 +369,10 @@ currentTokenParentParent is (kind: SyntaxKind.SimpleLambdaExpression or SyntaxKi var currentTokenParentParent = currentToken.Parent.Parent; // * { - in the member declaration context - if (currentToken.IsKind(SyntaxKind.OpenBraceToken) && currentTokenParentParent is MemberDeclarationSyntax) + // In top-level code, everything is a GlobalStatementSyntax which inherits from MemberDeclarationSyntax, + // but we don't want to format a plain open brace as though it's from a member declaration. + if (currentToken.IsKind(SyntaxKind.OpenBraceToken) && + currentTokenParentParent is MemberDeclarationSyntax and not GlobalStatementSyntax) { var option = currentTokenParentParent is BasePropertyDeclarationSyntax ? _options.NewLines.HasFlag(NewLinePlacement.BeforeOpenBraceInProperties) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/StructuredTriviaFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/StructuredTriviaFormattingRule.cs index aeb6e252c790..1cd578954ca0 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/StructuredTriviaFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/StructuredTriviaFormattingRule.cs @@ -25,6 +25,15 @@ internal sealed class StructuredTriviaFormattingRule : BaseFormattingRule { if (previousToken.Parent is StructuredTriviaSyntax || currentToken.Parent is StructuredTriviaSyntax) { + // File-based app directives use a '#:' prefix inside structured trivia, and the directive + // text is syntax-significant and must not be split by normal punctuation spacing. + if (currentToken.Parent == previousToken.Parent && + ((previousToken.Kind() == SyntaxKind.HashToken && currentToken.Kind() == SyntaxKind.ColonToken) || + (previousToken.Kind() == SyntaxKind.ColonToken && currentToken.Kind() == SyntaxKind.StringLiteralToken))) + { + return CreateAdjustSpacesOperation(space: 0, option: AdjustSpacesOption.ForceSpacesIfOnSingleLine); + } + // this doesn't take care of all cases where tokens belong to structured trivia. this is only for cases we care if (previousToken.Kind() == SyntaxKind.HashToken && SyntaxFacts.IsPreprocessorKeyword(currentToken.Kind())) { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/TokenBasedFormattingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/TokenBasedFormattingRule.cs index 39ac1aae8633..b6e6f8b56ee0 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/TokenBasedFormattingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Formatting/Rules/TokenBasedFormattingRule.cs @@ -380,7 +380,7 @@ is SyntaxKind.IdentifierToken or SyntaxKind.DefaultKeyword or SyntaxKind.BaseKeyword or SyntaxKind.ThisKeyword -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN or SyntaxKind.ExtensionKeyword #endif || previousToken.IsGenericGreaterThanToken() diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SemanticFacts/CSharpSemanticFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SemanticFacts/CSharpSemanticFacts.cs index 1066fc0d398a..1100a596ea0a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SemanticFacts/CSharpSemanticFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SemanticFacts/CSharpSemanticFacts.cs @@ -468,7 +468,7 @@ IdentifierNameSyntax nameSyntax when IsInPreprocessingSymbolContext(nameSyntax) }; private static IPreprocessingSymbol? CreatePreprocessingSymbol(SemanticModel model, SyntaxToken identifier) -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN => model.Compilation.CreatePreprocessingSymbol(identifier.ValueText); #else => null; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs index d7ff9f1f0172..d602211c12f5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/SyntaxFacts/CSharpSyntaxFacts.cs @@ -847,7 +847,7 @@ public string GetDisplayName(SyntaxNode? node, DisplayNameOptions options, strin return builder.ToString(); } -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN else if (memberDeclaration is ExtensionBlockDeclarationSyntax extensionDeclaration) { using var _ = PooledStringBuilder.GetInstance(out var builder); @@ -885,7 +885,7 @@ static void AppendTypeParameterList(StringBuilder builder, TypeParameterListSynt } } -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN void AppendParameterList(StringBuilder builder, ParameterListSyntax? parameterList) { if (parameterList != null) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index 2ee22d85f5a9..652d9c91681e 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -109,6 +109,12 @@ InternalUtilities\SemaphoreSlimExtensions.cs + + InternalUtilities\NoMessagePumpSyncContext.cs + + + InternalUtilities\SpecializedSyncContext.cs + InternalUtilities\StackGuard.cs diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxTreeExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxTreeExtensions.cs index 2752f6e9cd32..5ce77253a0fa 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxTreeExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/SyntaxTreeExtensions.cs @@ -242,7 +242,7 @@ public static bool IsGeneratedCode(this SyntaxTree syntaxTree, AnalyzerOptions? // Otherwise, fallback to generated code heuristic. return GeneratedCodeUtilities.IsGeneratedCode( - syntaxTree, t => syntaxFacts.IsRegularComment(t) || syntaxFacts.IsDocumentationComment(t), cancellationToken); + syntaxTree, syntaxFacts.IsRegularComment, cancellationToken); } /// diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs index 5a5b53deedcf..177e35942944 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs @@ -139,7 +139,7 @@ public static FlowGraphAnalysisData Create( static ImmutableArray GetParameters(ISymbol owningSymbol) { var parameters = owningSymbol.GetParameters(); -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN if (owningSymbol.ContainingSymbol is INamedTypeSymbol { IsExtension: true, ExtensionParameter: { } extensionParameter }) { return parameters.Add(extensionParameter); // order doesn't matter diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Formatting/Engine/AbstractFormatEngine.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Formatting/Engine/AbstractFormatEngine.cs index 9c517be3f04c..60fe40d4eb03 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Formatting/Engine/AbstractFormatEngine.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Formatting/Engine/AbstractFormatEngine.cs @@ -47,7 +47,7 @@ internal abstract partial class AbstractFormatEngine internal readonly TreeData TreeData; /// - /// It is very common to be formatting lots of documents at teh same time, with the same set of formatting rules and + /// It is very common to be formatting lots of documents at the same time, with the same set of formatting rules and /// options. To help with that, cache the last set of ChainedFormattingRules that was produced, as it is not a cheap /// type to create. /// diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs index 4900449830c5..ce1e07e5fe86 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/SyntaxFacts/ISyntaxFactsExtensions.cs @@ -871,6 +871,9 @@ public static bool IsRefExpression(this ISyntaxFacts syntaxFacts, [NotNullWhen(t public static bool IsSimpleMemberAccessExpression(this ISyntaxFacts syntaxFacts, [NotNullWhen(true)] SyntaxNode? node) => node != null && node.RawKind == syntaxFacts.SyntaxKinds.SimpleMemberAccessExpression; + public static bool IsSuppressNullableWarningExpression(this ISyntaxFacts syntaxFacts, [NotNullWhen(true)] SyntaxNode? node) + => node != null && node.RawKind == syntaxFacts.SyntaxKinds.SuppressNullableWarningExpression; + public static bool IsThisExpression(this ISyntaxFacts syntaxFacts, [NotNullWhen(true)] SyntaxNode? node) => node != null && node.RawKind == syntaxFacts.SyntaxKinds.ThisExpression; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.EventSymbolKey.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.EventSymbolKey.cs index 05db680762de..445fa401eb0b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.EventSymbolKey.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.EventSymbolKey.cs @@ -14,7 +14,7 @@ public sealed override void Create(IEventSymbol symbol, SymbolKeyWriter visitor) { visitor.WriteString(symbol.MetadataName); visitor.WriteSymbolKey(symbol.ContainingType); -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN visitor.WriteBoolean(symbol.PartialDefinitionPart is not null); #endif } @@ -24,7 +24,7 @@ protected sealed override SymbolKeyResolution Resolve( { var metadataName = reader.ReadString(); var containingTypeResolution = reader.ReadSymbolKey(contextualSymbol?.ContainingType, out var containingTypeFailureReason); -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN var isPartialImplementationPart = reader.ReadBoolean(); #endif @@ -36,7 +36,7 @@ protected sealed override SymbolKeyResolution Resolve( using var events = GetMembersOfNamedType(containingTypeResolution, metadataName); -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN if (isPartialImplementationPart) { for (var i = 0; i < events.Builder.Count; i++) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.NamedTypeSymbolKey.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.NamedTypeSymbolKey.cs index d6a1aed30748..eebf6246d41c 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.NamedTypeSymbolKey.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.NamedTypeSymbolKey.cs @@ -15,7 +15,7 @@ public sealed override void Create(INamedTypeSymbol symbol, SymbolKeyWriter visi visitor.WriteSymbolKey(symbol.ContainingSymbol); visitor.WriteString(symbol.Name); visitor.WriteInteger(symbol.Arity); -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN // Include the metadata name for extensions. We need this to uniquely find it when resolving. visitor.WriteString(symbol.IsExtension ? symbol.MetadataName : null); #endif @@ -35,7 +35,7 @@ protected sealed override SymbolKeyResolution Resolve( var containingSymbolResolution = reader.ReadSymbolKey(contextualSymbol?.ContainingSymbol, out var containingSymbolFailureReason); var name = reader.ReadRequiredString(); var arity = reader.ReadInteger(); -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN var extensionMetadataName = reader.ReadString(); #else string? extensionMetadataName = null; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.ParameterSymbolKey.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.ParameterSymbolKey.cs index e5ac0aa083f7..203d5a03388a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.ParameterSymbolKey.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.ParameterSymbolKey.cs @@ -69,7 +69,7 @@ protected sealed override SymbolKeyResolution Resolve( } break; -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN case INamedTypeSymbol { IsExtension: true, ExtensionParameter: { } extensionParameter }: Resolve(result, reader, metadataName, ordinal, [extensionParameter]); break; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.PreprocessingSymbolKey.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.PreprocessingSymbolKey.cs index a9e637b6a486..c9409e342324 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.PreprocessingSymbolKey.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/SymbolKey/SymbolKey.PreprocessingSymbolKey.cs @@ -15,7 +15,7 @@ public sealed override void Create(IPreprocessingSymbol symbol, SymbolKeyWriter protected sealed override SymbolKeyResolution Resolve(SymbolKeyReader reader, IPreprocessingSymbol? contextualSymbol, out string? failureReason) { -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN failureReason = null; return new SymbolKeyResolution(reader.Compilation.CreatePreprocessingSymbol(reader.ReadRequiredString())); #else diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AbstractSpeculationAnalyzer.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AbstractSpeculationAnalyzer.cs index 06bd70d4749d..5d79cabf4a7b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AbstractSpeculationAnalyzer.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/AbstractSpeculationAnalyzer.cs @@ -980,7 +980,7 @@ private bool IsCompatibleInterfaceMemberImplementation( // // The only cases where we feel confident enough to elide the cast are: // - // 1. When we have an Array/Delegate/Enum. These are such core types, and cannot be changed by teh user, + // 1. When we have an Array/Delegate/Enum. These are such core types, and cannot be changed by the user, // that we can trust their impls to not change. // 2. We have one of the builtin structs (like int). These are such core types, and cannot be changed by teh // user, that we can trust their impls to not change. diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ReferenceCountedDisposableCache.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ReferenceCountedDisposableCache.cs index 69a62f4531a1..f8f56ba0f0dd 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ReferenceCountedDisposableCache.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ReferenceCountedDisposableCache.cs @@ -15,9 +15,19 @@ namespace Roslyn.Utilities; internal sealed class ReferenceCountedDisposableCache where TValue : class, IDisposable where TKey : notnull { - private readonly Dictionary.WeakReference> _cache = []; + private readonly Dictionary.WeakReference> _cache; private readonly object _gate = new(); + public ReferenceCountedDisposableCache() + { + _cache = []; + } + + public ReferenceCountedDisposableCache(IEqualityComparer comparer) + { + _cache = new(comparer); + } + public IReferenceCountedDisposable> GetOrCreate(TKey key, Func valueCreator, TArg arg) { lock (_gate) @@ -58,7 +68,6 @@ public void Evict(TKey key) private sealed class Entry(ReferenceCountedDisposableCache cache, TKey key, TValue value) : IDisposable, ICacheEntry { - public TKey Key { get; } = key; public TValue Value { get; } = value; @@ -72,4 +81,15 @@ public void Dispose() Value.Dispose(); } } + + internal static class TestAccessor + { + public static IEnumerable GetCacheKeys(ReferenceCountedDisposableCache cache) + { + lock (cache._gate) + { + return [.. cache._cache.Keys]; + } + } + } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/StringBreaker.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/StringBreaker.cs index 798cbf5b91bd..d0d284dd8458 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/StringBreaker.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/StringBreaker.cs @@ -1,7 +1,8 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Diagnostics; using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Text; @@ -13,13 +14,13 @@ internal static class StringBreaker /// /// Breaks an identifier string into constituent parts. /// - public static void AddWordParts(string identifier, ref TemporaryArray parts) + public static void AddWordParts(ReadOnlySpan identifier, ref TemporaryArray parts) => AddParts(identifier, word: true, ref parts); - public static void AddCharacterParts(string identifier, ref TemporaryArray parts) + public static void AddCharacterParts(ReadOnlySpan identifier, ref TemporaryArray parts) => AddParts(identifier, word: false, ref parts); - public static void AddParts(string text, bool word, ref TemporaryArray parts) + public static void AddParts(ReadOnlySpan text, bool word, ref TemporaryArray parts) { for (var start = 0; start < text.Length;) { @@ -37,7 +38,7 @@ public static void AddParts(string text, bool word, ref TemporaryArray } } - public static TextSpan GenerateSpan(string identifier, int wordStart, bool word) + public static TextSpan GenerateSpan(ReadOnlySpan identifier, int wordStart, bool word) { var length = identifier.Length; wordStart = SkipPunctuation(identifier, length, wordStart); @@ -77,7 +78,7 @@ public static TextSpan GenerateSpan(string identifier, int wordStart, bool word) return default; } - private static TextSpan ScanCharacterRun(string identifier, int length, int wordStart) + private static TextSpan ScanCharacterRun(ReadOnlySpan identifier, int length, int wordStart) { // In a character run, if we have XMLDocument, then we will break that up into // X, M, L, and Document. @@ -98,7 +99,7 @@ private static TextSpan ScanCharacterRun(string identifier, int length, int word } } - private static TextSpan ScanWordRun(string identifier, int length, int wordStart) + private static TextSpan ScanWordRun(ReadOnlySpan identifier, int length, int wordStart) { // In a word run, if we have XMLDocument, then we will break that up into // XML and Document. @@ -147,7 +148,7 @@ private static TextSpan ScanWordRun(string identifier, int length, int wordStart } } - private static TextSpan ScanLowerCaseRun(string identifier, int length, int wordStart) + private static TextSpan ScanLowerCaseRun(ReadOnlySpan identifier, int length, int wordStart) { var current = wordStart + 1; while (current < length && IsLower(identifier[current])) @@ -158,7 +159,7 @@ private static TextSpan ScanLowerCaseRun(string identifier, int length, int word return new TextSpan(wordStart, current - wordStart); } - private static TextSpan ScanNumber(string identifier, int length, int wordStart) + private static TextSpan ScanNumber(ReadOnlySpan identifier, int length, int wordStart) { var current = wordStart + 1; while (current < length && char.IsDigit(identifier[current])) @@ -169,7 +170,7 @@ private static TextSpan ScanNumber(string identifier, int length, int wordStart) return TextSpan.FromBounds(wordStart, current); } - private static int SkipPunctuation(string identifier, int length, int wordStart) + private static int SkipPunctuation(ReadOnlySpan identifier, int length, int wordStart) { while (wordStart < length) { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ValueTaskExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ValueTaskExtensions.cs index e076ea250d58..46f608755d6d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ValueTaskExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ValueTaskExtensions.cs @@ -17,6 +17,22 @@ internal static class ValueTaskExtensions /// calling it from a synchronous method where you know it should have completed synchronously. This is an easy /// way to assert that while silencing any compiler complaints. /// + public static void VerifyCompleted(this ValueTask task, string message = "ValueTask should have already been completed") + { + Contract.ThrowIfFalse(task.IsCompleted, message); + + // Propagate any exceptions that may have been thrown. + task.GetAwaiter().GetResult(); + } + + /// + /// Asserts the passed has already been completed. + /// + /// + /// This is useful for a specific case: sometimes you might be calling an API that is "sometimes" async, and you're + /// calling it from a synchronous method where you know it should have completed synchronously. This is an easy + /// way to assert that while silencing any compiler complaints. + /// public static T VerifyCompleted(this ValueTask task, string message = "ValueTask should have already been completed") { Contract.ThrowIfFalse(task.IsCompleted, message); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/WordSimilarityChecker.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/WordSimilarityChecker.cs index b323c4ddb481..7e4784f6b845 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/WordSimilarityChecker.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/WordSimilarityChecker.cs @@ -76,15 +76,36 @@ public static bool AreSimilar(string originalText, string candidateText, bool su return result; } + /// + /// Maximum allowed edit distance for a fuzzy match given the source length. These tiers + /// match Elasticsearch/Lucene's widely-deployed fuzziness: AUTO setting + /// (), + /// which is grounded in Damerau's finding that ~80% of human misspellings are edit distance 1: + /// + /// Length 1–2: no fuzzy matching (see ) + /// Length 3–5: threshold 1 + /// Length 6+: threshold 2 + /// + /// internal static int GetThreshold(string value) - => value.Length <= 4 ? 1 : 2; + => GetThreshold(value.Length); + + /// + internal static int GetThreshold(int length) + => length <= 5 ? 1 : 2; + + /// + /// Minimum source length for fuzzy matching to be attempted. Patterns shorter than this + /// produce too many spurious hits. See . + /// + internal const int MinFuzzyLength = 3; public bool AreSimilar(string candidateText) => AreSimilar(candidateText, out _); public bool AreSimilar(string candidateText, out double similarityWeight) { - if (_source.Length < 3) + if (_source.Length < MinFuzzyLength) { // If we're comparing strings that are too short, we'll find // far too many spurious hits. Don't even bother in this case. diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs index d79ea882a5eb..cc10ba0281af 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/IMethodSymbolExtensions.cs @@ -15,7 +15,7 @@ namespace Microsoft.CodeAnalysis.Shared.Extensions; internal static partial class IMethodSymbolExtensions { -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN internal static IMethodSymbol? TryGetCorrespondingExtensionBlockMethod(this IMethodSymbol methodSymbol) { if (methodSymbol is { IsStatic: true, IsImplicitlyDeclared: true, ContainingType.MightContainExtensionMethods: true }) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/ISymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/ISymbolExtensions.cs index 8d07a2fe8754..4f299c49dfe0 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/ISymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/ISymbolExtensions.cs @@ -893,7 +893,7 @@ public static bool IsClassicOrModernInstanceExtensionMethod( return true; } -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN if (method is { IsStatic: false, AssociatedExtensionImplementation: { } associatedMethod }) { classicExtensionMethod = associatedMethod; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/SymbolEquivalenceComparer.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/SymbolEquivalenceComparer.cs index 2f0ccbfdfc50..33fb4682dbeb 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/SymbolEquivalenceComparer.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Extensions/Symbols/SymbolEquivalenceComparer.cs @@ -241,14 +241,14 @@ private static bool IsPartialPropertyImplementationPart(IPropertySymbol symbol) => symbol.PartialDefinitionPart != null; private static bool IsPartialEventDefinitionPart(IEventSymbol symbol) -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN => symbol.PartialImplementationPart != null; #else => false; #endif private static bool IsPartialEventImplementationPart(IEventSymbol symbol) -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN => symbol.PartialDefinitionPart != null; #else => false; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb index 645cbc00ee32..ef99a2ce4080 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Extensions/ExpressionSyntaxExtensions.vb @@ -148,7 +148,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Extensions ' e.g. `WriteLine(SomeMethod)` is equivalent to `WriteLine(SomeMethod())`. So infer the return ' type of 'SomeMethod' here, not a delegate type for it. ' - ' Note: this does not apply if teh user wrote `WriteLine(AddressOf SomeMethod)` of course. + ' Note: this does not apply if the user wrote `WriteLine(AddressOf SomeMethod)` of course. If expression IsNot Nothing Then Dim symbolInfo = semanticModel.GetSymbolInfo(expression, cancellationToken) Dim methodSymbol = TryCast(symbolInfo.GetAnySymbol(), IMethodSymbol) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SemanticFacts/VisualBasicSemanticFacts.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SemanticFacts/VisualBasicSemanticFacts.vb index c1de880adbb9..bcfbdafd3e74 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SemanticFacts/VisualBasicSemanticFacts.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/SemanticFacts/VisualBasicSemanticFacts.vb @@ -308,7 +308,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic End Function Private Shared Function CreatePreprocessingSymbol(model As SemanticModel, token As SyntaxToken) As IPreprocessingSymbol -#If Not ROSLYN_4_12_OR_LOWER Then +#If Not OLDER_ROSLYN Then Return model.Compilation.CreatePreprocessingSymbol(token.ValueText) #Else return nothing diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/SyntaxTreeExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/SyntaxTreeExtensions.cs index 9bb67659a17b..49acaed10e8f 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/SyntaxTreeExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/ContextQuery/SyntaxTreeExtensions.cs @@ -355,7 +355,7 @@ public static bool IsLocalFunctionDeclarationContext( var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); var token = leftToken.GetPreviousTokenIfTouchingWord(position); - // if we're after an attribute, restart the check at teh start of the attribute. + // if we're after an attribute, restart the check at the start of the attribute. if (token.Kind() == SyntaxKind.CloseBracketToken && token.Parent is AttributeListSyntax) return syntaxTree.IsLocalFunctionDeclarationContext(token.Parent.SpanStart, validModifiers, cancellationToken); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/WithElementSyntaxExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/WithElementSyntaxExtensions.cs index 1510033d4858..c4c530e6efb9 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/WithElementSyntaxExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Extensions/WithElementSyntaxExtensions.cs @@ -13,7 +13,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Extensions; internal static class WithElementSyntaxExtensions { -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN public static ImmutableArray GetCreationMethods( this WithElementSyntax? withElement, SemanticModel semanticModel, CancellationToken cancellationToken) { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/LanguageServices/CSharpTypeInferenceService.TypeInferrer.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/LanguageServices/CSharpTypeInferenceService.TypeInferrer.cs index dfff7060c2c8..f5f33cc00340 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/LanguageServices/CSharpTypeInferenceService.TypeInferrer.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/LanguageServices/CSharpTypeInferenceService.TypeInferrer.cs @@ -1799,7 +1799,7 @@ private IEnumerable InferTypeForExpressionOfMemberAccessExpre // // await goo.ConfigureAwait() // - // then we can figure out what 'goo' should be based on teh await + // then we can figure out what 'goo' should be based on the await // context. var name = memberAccessExpression.Name.Identifier.Value; if (name.Equals(nameof(Task<>.ConfigureAwait)) && diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Utilities/SyntaxKindSet.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Utilities/SyntaxKindSet.cs index c1e0a0be0d0e..7a37dde0bcca 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Utilities/SyntaxKindSet.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/Utilities/SyntaxKindSet.cs @@ -73,6 +73,9 @@ internal sealed class SyntaxKindSet SyntaxKind.RecordDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.RecordStructDeclaration, +#if !OLDER_ROSLYN + SyntaxKind.UnionDeclaration, +#endif SyntaxKind.EnumDeclaration, }; @@ -83,13 +86,17 @@ internal sealed class SyntaxKindSet SyntaxKind.RecordDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.RecordStructDeclaration, +#if !OLDER_ROSLYN + SyntaxKind.UnionDeclaration, +#endif }; public static readonly ISet NonEnumTypeDeclarations = new HashSet(SyntaxFacts.EqualityComparer) { SyntaxKind.ClassDeclaration, -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN SyntaxKind.ExtensionBlockDeclaration, + SyntaxKind.UnionDeclaration, #endif SyntaxKind.InterfaceDeclaration, SyntaxKind.RecordDeclaration, @@ -116,12 +123,18 @@ internal sealed class SyntaxKindSet SyntaxKind.RecordDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.RecordStructDeclaration, +#if !OLDER_ROSLYN + SyntaxKind.UnionDeclaration, +#endif }; public static readonly ISet StructOnlyTypeDeclarations = new HashSet(SyntaxFacts.EqualityComparer) { SyntaxKind.StructDeclaration, SyntaxKind.RecordStructDeclaration, +#if !OLDER_ROSLYN + SyntaxKind.UnionDeclaration, +#endif }; public static readonly ISet InterfaceOnlyTypeDeclarations = new HashSet(SyntaxFacts.EqualityComparer) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/AbstractCodeGenerationService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/AbstractCodeGenerationService.cs index 10d45074d8c3..6d84fad3ddfd 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/AbstractCodeGenerationService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/AbstractCodeGenerationService.cs @@ -233,7 +233,7 @@ private async Task GetEditAsync( CancellationToken cancellationToken) { var (destinationDeclaration, availableIndices) = - FindMostRelevantDeclaration(context.Solution, destination, context.Context.BestLocation, cancellationToken); + FindMostRelevantDeclaration(context.Solution, destination, context.Context, context.Context.BestLocation, cancellationToken); if (destinationDeclaration == null) throw new ArgumentException(WorkspaceExtensionsResources.Could_not_find_location_to_generation_symbol_into); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/AbstractCodeGenerationService_FindDeclaration.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/AbstractCodeGenerationService_FindDeclaration.cs index ae0d5d0109fd..a081b024752b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/AbstractCodeGenerationService_FindDeclaration.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/AbstractCodeGenerationService_FindDeclaration.cs @@ -23,9 +23,12 @@ internal abstract partial class AbstractCodeGenerationService GetAvailableInsertionIndices((SyntaxNode)destination, cancellationToken); public bool CanAddTo(ISymbol destination, Solution solution, CancellationToken cancellationToken) + => CanAddTo(destination, solution, CodeGenerationContext.Default, cancellationToken); + + public bool CanAddTo(ISymbol destination, Solution solution, CodeGenerationContext context, CancellationToken cancellationToken) { var declarations = _symbolDeclarationService.GetDeclarations(destination); - return declarations.Any(static (r, arg) => arg.self.CanAddTo(r.GetSyntax(arg.cancellationToken), arg.solution, arg.cancellationToken), (self: this, solution, cancellationToken)); + return declarations.Any(static (r, arg) => arg.self.CanAddTo(r.GetSyntax(arg.cancellationToken), arg.solution, arg.context, arg.cancellationToken), (self: this, solution, context, cancellationToken)); } protected static SyntaxToken GetEndToken(SyntaxNode node) @@ -53,11 +56,15 @@ protected static TextSpan GetSpan(SyntaxNode node) } public bool CanAddTo(SyntaxNode destination, Solution solution, CancellationToken cancellationToken) - => CanAddTo(destination, solution, cancellationToken, out _); + => CanAddTo(destination, solution, CodeGenerationContext.Default, cancellationToken); + + public bool CanAddTo(SyntaxNode destination, Solution solution, CodeGenerationContext context, CancellationToken cancellationToken) + => CanAddTo(destination, solution, context, cancellationToken, out _); private bool CanAddTo( SyntaxNode? destination, Solution solution, + CodeGenerationContext context, CancellationToken cancellationToken, out IList? availableIndices, bool checkGeneratedCode = false) @@ -76,6 +83,11 @@ private bool CanAddTo( return false; } + if (context.AllowGenerationIntoHiddenCode?.Invoke(document) == true) + { + return true; + } + // We can never generate into a document from a source generator, because those are immutable if (document is SourceGeneratedDocument) { @@ -134,13 +146,14 @@ private bool CanAddTo( Location? location, CancellationToken cancellationToken) { - var (declaration, _) = FindMostRelevantDeclaration(solution, namespaceOrType, location, cancellationToken); + var (declaration, _) = FindMostRelevantDeclaration(solution, namespaceOrType, CodeGenerationContext.Default, location, cancellationToken); return declaration; } private (SyntaxNode? declaration, IList? availableIndices) FindMostRelevantDeclaration( Solution solution, INamespaceOrTypeSymbol namespaceOrType, + CodeGenerationContext context, Location? location, CancellationToken cancellationToken) { @@ -225,7 +238,7 @@ bool TryAddToWorker( if (predicate(decl)) { fallbackDeclaration ??= decl; - if (CanAddTo(decl, solution, cancellationToken, out availableIndices, checkGeneratedCode)) + if (CanAddTo(decl, solution, context, cancellationToken, out availableIndices, checkGeneratedCode)) { declaration = decl; return true; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationContext.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationContext.cs index 0a0d845c4be3..69be876c45b0 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationContext.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationContext.cs @@ -130,6 +130,11 @@ internal sealed class CodeGenerationContext /// public bool ReuseSyntax { get; } + /// + /// Allows code generation into locations that would otherwise be rejected for being hidden or source-generated. + /// + public Func? AllowGenerationIntoHiddenCode { get; } + public CodeGenerationContext( Location? contextLocation = null, Location? afterThisLocation = null, @@ -144,7 +149,8 @@ public CodeGenerationContext( bool generateDocumentationComments = false, bool autoInsertionLocation = true, bool sortMembers = true, - bool reuseSyntax = false) + bool reuseSyntax = false, + Func? allowGenerationIntoHiddenCode = null) { CheckLocation(contextLocation, nameof(contextLocation)); CheckLocation(afterThisLocation, nameof(afterThisLocation)); @@ -163,6 +169,7 @@ public CodeGenerationContext( AutoInsertionLocation = autoInsertionLocation; SortMembers = sortMembers; ReuseSyntax = reuseSyntax; + AllowGenerationIntoHiddenCode = allowGenerationIntoHiddenCode; } private static void CheckLocation(Location? location, string name) @@ -190,7 +197,8 @@ public CodeGenerationContext With( Optional generateDocumentationComments = default, Optional autoInsertionLocation = default, Optional sortMembers = default, - Optional reuseSyntax = default) + Optional reuseSyntax = default, + Optional?> allowGenerationIntoHiddenCode = default) { var newContextLocation = contextLocation.HasValue ? contextLocation.Value : this.ContextLocation; var newAfterThisLocation = afterThisLocation.HasValue ? afterThisLocation.Value : this.AfterThisLocation; @@ -206,6 +214,7 @@ public CodeGenerationContext With( var newAutoInsertionLocation = autoInsertionLocation.HasValue ? autoInsertionLocation.Value : this.AutoInsertionLocation; var newSortMembers = sortMembers.HasValue ? sortMembers.Value : this.SortMembers; var newReuseSyntax = reuseSyntax.HasValue ? reuseSyntax.Value : this.ReuseSyntax; + var newAllowGenerationIntoHiddenCode = allowGenerationIntoHiddenCode.HasValue ? allowGenerationIntoHiddenCode.Value : this.AllowGenerationIntoHiddenCode; return new CodeGenerationContext( newContextLocation, @@ -221,6 +230,7 @@ public CodeGenerationContext With( newGenerateDocumentationComments, newAutoInsertionLocation, newSortMembers, - newReuseSyntax); + newReuseSyntax, + newAllowGenerationIntoHiddenCode); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerator.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerator.cs index b244cc32132a..804f9902a67b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerator.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerator.cs @@ -90,5 +90,11 @@ public static Task AddMemberDeclarationsAsync(CodeGenerationSolutionCo /// Returns true if additional declarations can be added to the destination symbol's declaration. /// public static bool CanAdd(Solution solution, ISymbol destination, CancellationToken cancellationToken) - => GetCodeGenerationService(solution, destination.Language).CanAddTo(destination, solution, cancellationToken); + => CanAdd(solution, destination, CodeGenerationContext.Default, cancellationToken); + + /// + /// Returns true if additional declarations can be added to the destination symbol's declaration. + /// + public static bool CanAdd(Solution solution, ISymbol destination, CodeGenerationContext context, CancellationToken cancellationToken) + => GetCodeGenerationService(solution, destination.Language).CanAddTo(destination, solution, context, cancellationToken); } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/ICodeGenerationService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/ICodeGenerationService.cs index 7483075918a9..f12818b2dea0 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/ICodeGenerationService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/ICodeGenerationService.cs @@ -177,11 +177,21 @@ internal interface ICodeGenerationService : ILanguageService /// bool CanAddTo(ISymbol destination, Solution solution, CancellationToken cancellationToken); + /// + /// true if destination is a location where other symbols can be added to. + /// + bool CanAddTo(ISymbol destination, Solution solution, CodeGenerationContext context, CancellationToken cancellationToken); + /// /// true if destination is a location where other symbols can be added to. /// bool CanAddTo(SyntaxNode destination, Solution solution, CancellationToken cancellationToken); + /// + /// true if destination is a location where other symbols can be added to. + /// + bool CanAddTo(SyntaxNode destination, Solution solution, CodeGenerationContext context, CancellationToken cancellationToken); + /// /// Return the most relevant declaration to namespaceOrType, /// it will first search the context node contained within, diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs index 63c696b27e2f..0d0b076e59fe 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs @@ -99,7 +99,7 @@ public virtual ImmutableArray ReturnTypeCustomModifiers public bool IsConditional => false; -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN public bool IsIterator => false; public IMethodSymbol AssociatedExtensionImplementation => null; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs index b8dc0f9e3806..ea4521fa187e 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs @@ -124,7 +124,7 @@ public override string MetadataName public bool IsFileLocal => Modifiers.IsFile; -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN public bool IsExtension => false; public string ExtensionGroupingName => null; public string ExtensionMarkerName => null; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationEventSymbol.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationEventSymbol.cs index bb9b4f82e8da..d22ccdebb870 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationEventSymbol.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationEventSymbol.cs @@ -55,7 +55,7 @@ public override TResult Accept(SymbolVisitor null; -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN public IEventSymbol? PartialImplementationPart => null; public IEventSymbol? PartialDefinitionPart => null; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationTypeSymbol.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationTypeSymbol.cs index 6e1028351556..3994f2bdb914 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationTypeSymbol.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationTypeSymbol.cs @@ -42,7 +42,7 @@ public ImmutableArray AllInterfaces public bool IsNativeIntegerType => false; -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN bool ITypeSymbol.IsExtension => false; IParameterSymbol ITypeSymbol.ExtensionParameter => null; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/ISymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/ISymbolExtensions.cs index dff344d216b5..f9bd26a6c2d5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/ISymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/ISymbolExtensions.cs @@ -20,7 +20,7 @@ public static bool IsPartial(this ISymbol symbol) if (isPartial) return true; -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN return symbol is IEventSymbol @event && (@event.PartialDefinitionPart != null || @event.PartialImplementationPart != null); #else @@ -41,7 +41,7 @@ public static DeclarationModifiers GetSymbolModifiers(this ISymbol symbol) .WithPartial(symbol.IsPartial()); } -#if !ROSLYN_4_12_OR_LOWER +#if !OLDER_ROSLYN public static ISymbol? ReduceExtensionMember(this ISymbol? member, ITypeSymbol? receiverType) { if (member is null || receiverType is null) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/AddImports/AddImportHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/AddImports/AddImportHelpers.cs index 50d19ed10b96..93863ea39f7a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/AddImports/AddImportHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/LanguageServices/AddImports/AddImportHelpers.cs @@ -72,7 +72,7 @@ public static (TRootSyntax root, bool addBlankLine) MoveTrivia #else _workspace.RegisterWorkspaceChangedHandler((e) => @@ -84,7 +84,7 @@ public SemanticModelReuseWorkspaceService(Workspace workspace) } } } -#if ROSLYN_4_12_OR_LOWER +#if OLDER_ROSLYN ; #else ); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Simplification/AbstractSimplificationService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Simplification/AbstractSimplificationService.cs index 2d7c6fd29bb9..25d6edd1c747 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Simplification/AbstractSimplificationService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Simplification/AbstractSimplificationService.cs @@ -219,9 +219,12 @@ async ValueTask ReduceOneNodeOrTokenAsync( ? nodeOrToken.Parent.ReplaceNode(nodeOrToken.AsNode()!, currentNodeOrToken.AsNode()!) : nodeOrToken.Parent.ReplaceToken(nodeOrToken.AsToken(), currentNodeOrToken.AsToken()); + // Use .First() instead of .Single() to avoid walking the entire enumerable. + // The Debug.Assert below catches if the invariant of exactly one result is ever broken. currentNodeOrToken = replacedParent - .ChildNodesAndTokens() - .Single(c => c.HasAnnotation(annotation)); + .GetAnnotatedNodesAndTokens(annotation) + .First(); + Debug.Assert(replacedParent.GetAnnotatedNodesAndTokens(annotation).Count() == 1); } if (isNode) @@ -235,7 +238,12 @@ async ValueTask ReduceOneNodeOrTokenAsync( var newDocument = document.WithSyntaxRoot(newRoot); semanticModelForReduce = await newDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); newRoot = await semanticModelForReduce.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - currentNodeOrToken = newRoot.DescendantNodes().Single(c => c.HasAnnotation(marker)); + // Use .First() instead of .Single() to avoid walking the entire enumerable. + // The Debug.Assert below catches if the invariant of exactly one result is ever broken. + currentNodeOrToken = newRoot + .GetAnnotatedNodesAndTokens(marker) + .First(); + Debug.Assert(newRoot.GetAnnotatedNodesAndTokens(marker).Count() == 1); } else { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Workspace/Mef/MefLanguageServices.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Workspace/Mef/MefLanguageServices.cs index de1198d6db9b..16cc10fe0481 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Workspace/Mef/MefLanguageServices.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Workspace/Mef/MefLanguageServices.cs @@ -49,11 +49,6 @@ public MefLanguageServices( public override string Language => _language; - public bool HasServices - { - get { return _services.Length > 0; } - } - public override void Dispose() { ImmutableArray disposableServices; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Workspace/Mef/MefWorkspaceServices.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Workspace/Mef/MefWorkspaceServices.cs index 68901dbc1bec..3b8c3ef1ada6 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Workspace/Mef/MefWorkspaceServices.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Workspace/Mef/MefWorkspaceServices.cs @@ -180,17 +180,7 @@ public override HostLanguageServices GetLanguageServices(string languageName) languageServices = ImmutableInterlocked.GetOrAdd(ref _languageServicesMap, languageName, static (languageName, self) => new MefLanguageServices(self, languageName), this); } - if (languageServices.HasServices) - { - return languageServices; - } - else - { - // throws exception -#pragma warning disable RS0030 // Do not used banned API 'GetLanguageServices', use 'GetExtendedLanguageServices' instead - allowed in this context. - return base.GetLanguageServices(languageName); -#pragma warning restore RS0030 // Do not used banned APIs - } + return languageServices; } public override IEnumerable FindLanguageServices(MetadataFilter filter) diff --git a/src/Workspaces/VisualBasicTest/Microsoft.CodeAnalysis.VisualBasic.Workspaces.UnitTests.vbproj b/src/Workspaces/VisualBasicTest/Microsoft.CodeAnalysis.VisualBasic.Workspaces.UnitTests.vbproj index 16cbc5e051c2..b466f9711d3d 100644 --- a/src/Workspaces/VisualBasicTest/Microsoft.CodeAnalysis.VisualBasic.Workspaces.UnitTests.vbproj +++ b/src/Workspaces/VisualBasicTest/Microsoft.CodeAnalysis.VisualBasic.Workspaces.UnitTests.vbproj @@ -5,7 +5,7 @@ Library Off - $(NetVSShared);net472 + $(NetRoslynWindowsTests);net472